1. 命令优先级
首先,输入命令未定义时,输出命令不存在,替换下面的 cmd 为具体的命令名称。
Command 'cmd' not found
例如输入 logged out,由于命令 logged 未定义(注意:这里的out作为参数而不是命令的一部分出现),所以输出
Command 'logged' not found
其次,当输入命令有定义但参数个数不合法时,输出
Illegal argument count
当命令有定义,参数个数正确时,才会进一步输出 Already logged in、Bye ~ 等成功或失败信息。
当一句命令存在多种非法情况,按上述顺序,只输出最先发生的非法信息。
2. 实现思路
这里的实现思路仅供参考,具体同学们可以自行发挥。🙂
2.1 Java的输入与输出
一般我们会采用Scanner类处理Java的输入问题。如果我们的输入流是System.in(标准输入流),那么我们会这样实例化:
Scanner s = new Scanner(System.in); // System.in可以被替换为其他的流
一般我们使用next(), nextLine()来获取输入,并辅以hasNext(), hasNextLine()进行是否输入完成的判断。
import java.util.Scanner;
public class Test{
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
// next()版
while(scan.hasNext()){
String str = scan.next();
System.out.println("get data: " + str);
}
// nextLine()版
while(scan.hasNextLine()){
String str = scan.nextLine();
//按照空格进行分割
String[] strs = str.split("\\s+");
for(String tstr : strs){
System.out.println("get data: " + tstr);
}
}
// 养成良好的编码习惯.
scan.close();
}
}
你可以尝试使用带参数的hasNext().
输出十分简单,在我们的迭代中,System.out.print()或System.out.println()基本足够。
如果你想要更方便地进行本地的测试,将输入输出重定向到文件中也许是个不错的选择。在迭代一中,并不会对文件的重定向做强制性要求。😜
绝对,绝对,绝对不要在同一个流上,创建多个
Scanner!!!⚠️
2.2 实体类
以用户为例子。你可以设计一个 User 类(不限定类的名字,但一个好的名字是十分重要的),该类至少包括如下属性:
- 学号/教工号
- 姓名
- 密码
- 身份类型(可以通过一个字段表示,以此字段标识不同的身份)
建议将属性设置为私有属性(private),并为其编写相应的 getter/setter。
2.3 工具类
对于一些具有一定通用性或者不依附实体对象的方法,例如检查卡号格式、检查密码格式、判断登录状态、判断注册状态、格式化输出实体类等,可以设计一些工具类,在其中实现这些方法,一般设计为静态方法即可。你可以认为,工具类是一个存储各种工具的类,这些工具就是方法,而这些工作是随时都可以被共享的。
如果某些同学提前接触过多线程编程,应该知道,有时候一个方法,一个属性,甚至一个类,都是可以被**独享的。😲
2.4 全局变量管理
为了完成实验,同学们不免要设计一些全局变量。对于全局变量的管理,同学们可以设计一个类,相关的全局变量设置为 static;也可以设计一个类,而将全局变量设置为成员变量,之后通过创建一个对象来操作全局变量。(Singleton模式)
一般来说,全局变量主要用于进行 某些数据的临时存储, 你可以认为管理全局变量的类是一个”仓库类“。
部分同学的大作业可能涉及到数据的持久存储。你可以使用文件系统或者数据库达成这个目的。😃
2.5 正则表达式
在判断姓名,密码是否合法时,建议大家用正则表达式来判断。例如,想要判断名字是否合法时,可以参考下列代码
public boolean judgeName(String name) {
String name_pattern = "[A-Za-z][A-Za-z_]{3,15}";
if (!name.matches(name_pattern)) {
return false;
}
return true;
}
正则表达式通常用于匹配字符串模式,不太适合处理范围验证,所以对于卡号的判断,可以分割字符串后再验证范围。正则表达式的更多用法,大家可以查看:正则表达式 - 教程。
2.6 List、Map 等的使用
在开发时, 合理使用List和Map加快我们的开发效率。
对于List ,常用的方式如下
public class Test {
public static void main(String[] args) {
// List<YourClass> list = new LinkedList<>(); // 这个是链表(回忆一下数据结构)
List<TargetClass> list = new ArrayList<>(); // 一般来说,用ArrayList<>()
list.add(targetObject); // 在列表的尾部插入数据
list.remove(index); // 根据下标删除元素。注意,index >= 0 && index < list.size()
}
}
- ArrayList
- 底层原理:基于可变大小的数组实现,数组扩容时会创建新数组并复制旧数据。
- 适用场景:当需要频繁查询元素,且元素数量变化不大时效率高。
- LinkedList
- 底层原理:每个元素都是一个节点,包含前驱和后继节点的引用,形成双向链表。
- 适用场景:适合于频繁的插入和删除操作,尤其是在列表的开始或结尾。
Java集合之List
对于Map ,常用的方式如下
public class Test {
static class Person {
public final String id;
public String name;
public Person(String id, String name) {
this.id = id;
this.name = name;
}
}
public static void main(String[] args) {
// Map<key, value> map = new HashMap<>();
// Map<Key, value> map = new TreeMap<>();
Map<String, Person> map = new HashMap<>();
Person person = new Person("1234", "xiaoming");
map.put(person.id, person); // 向表中添加数据
map.containsKey(person.id); // 判断key是否存在
Person p = map.get(person.id); // 根据key获取value
map.remove(person.id); // 删除key对应的value
}
}
更多的使用方法同学们可以查阅相关资料,这里就不展开了。List和Map还涉及到“泛型”,感兴趣的同学可以提前了解了解。实际上,在我们给出的实际中,List<TargetClass>,Map<String, Person>的尖括号的使用,其实就对应了泛型。
可以看到,在使用
List和Map的时候,尖括号会传入一个类。因为Java面向对象的特性,基本数据类型(int,double,boolean)是不可以传入的,必须传入一个类。而每个基本数据类型都会对应一个类,例如int对应Integer,所以同学们在使用到List和Map,需要传入基本数据类型的时候,要记得把基本数据类型换为其对应的类。
List 和 Map 的区别:
List:它是一个有序集合,可以包含重复的元素。它主要用于存储一系列顺序排列的对象。Map:它是一个键值对集合,用于存储键和值之间的映射,其中键是唯一的,而值可以重复。
HashMap 与 TreeMap 的区别:
HashMap和TreeMap都是Map接口的实现,但它们在内部实现和性能方面有所不同:
- 顺序:
HashMap:不保证元素的顺序,因为元素的顺序取决于哈希码和数组的索引位置。TreeMap:根据元素的键来自然排序,或者根据创建TreeMap时提供的Comparator来排序。
- 性能:
HashMap:通常提供比TreeMap更快的查找、插入和删除操作,时间复杂度接近O(1)。TreeMap:由于需要维护元素的排序,其操作的时间复杂度为O(log n),其中n是元素的数量。
- null 值:
HashMap:允许使用一个null键和多个null值。TreeMap:不允许使用null键,但可以允许多个null值。
2.7 ?集合排序
// users 是一个 HashMap<String, User>,其键为 user id
// getNumberUserId() 返回字符串 id 的数字部分
List<Map.Entry<String, User>> list = new ArrayList<>(users.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, User>>() {
@Override
public int compare(Map.Entry<String, User> u1,
Map.Entry<String, User> u2){
// 顺序排序,如果想要逆序,u2 - u1 即可
return getNumberUserId(u1.getKey()) -
getNumberUserId(u2.getKey());
}
});
// 也可以用 lambda 表达式替换匿名类这种写法
// comparingInt 的参数是一个函数
// 这个函数接受一个泛型(这里是 Map.Entry<String, User>),返回一个 int
// 该排序就根据 List 中每个元素返回的 int 值进行,默认为顺序
// 如果想要逆序,在返回值前添加一个负号即可
list.sort(Comparator.comparingInt(o -> getNumberUserId(o.getKey())));
*3 面向对象中的设计思想
这一板块将会介绍一些在进行面向对象编程时,比较好的设计思想。这部分的内容 也是作为参考, 同学们如果不采纳并不会造成任何负面影响(事实上,如果你只是单纯的抄袭,而没有加上自己的思考,代码会更不好编写)。此模块存在的意义,实际上是想让同学们能够更深入地了解OOP,而不是只会编写Java代码,或者应付考试。
3.1 封装(Encapsulation)
在前文中,我们提到过关于实体类的设计。简单来说,实体类就是直接对应现实生活中存在的物品或者概念的类。一般而言,我们会遵循如下的方法设计一个实体类:
public class Entity{
private TYPE1 attribute1;
private TYPE2 attribute2;
//...
public Entity(TYPE1 att1, TYPE2 att2, ...){
this.attribute1 = att1;
this.attribute2 = att2;
//...
}
public TYPE1 getAttribute1(){
return this.attribute1;
}
public void setAttribute1(TYPE1 att1){
this.attribute1 = att1;
}
//...
}
如果你使用的是IDEA作为IDE,那么可以按下
alt+Insert,便可以选择自动生成各种getter(), setter(), toString(), equals()以及构造方法。
为什么要这么做呢?实际上,对于一个小项目来说,你确实大可不必降属性封装为private并且设计相应的getter(),setter(),这样在一定程度上会给你的编码带来便利,尤其是当你设置其访问控制权限为public时,你可以在同项目下的任何一个文件,任何一个类中访问并使用那个属性,岂不快哉?
但是,随着类的数量的增多,这样做的麻烦逐渐显现:
- 类与类之间无封装可言,大家都是透明的,可以肆意访问并修改各自的属性,这就会导致混乱,而且对属性的访问和修改也不是很明显(比较
a = 1 , setA(1)),这会增大DEBUG难度 - 一般来说,我们只会提供能够完成一个方法的最少但是必要的信息。例如,现在有一个方法需要知道当前有多少用户(假定有一个
List用于存储用户),那么我们只需要传入这个List的长度,而不需要将整个List都传入,然后再求长度。一方面,这样可能会造成不必要的内存开销(值传递和引用传递的问题,虽然在Java中这类问题基本不存在),另一方面,传入List后,这个方法就可以修改其内容,这可能会导致不必要的麻烦 - 难以进行合法性判断。如果在访问和修改某个属性时,需要考虑到类似于权限和值的合法性问题,我们总是会写一个专门的合法性判断方法,甚至创造一个合法性判断类。但是这显然是没必要的,一个类的属性的合法性判断工作没有任何理由交给其他的类担任,相关的工作我们完全可以在
setter(),getter()中胜任。以下给出一个很简单的例子:
public Exam{
private Integer grade;
public Integer getter(){
return this.grade;
}
public void setter(String person, Integer grade){
if(person.equals("Teacher") && grade >= 0 && grade <= 100){
this.grade = grade;
}
}
}
setter()方法的返回值不一定非要是void,你可以用返回值来表示执行的状态,发生了什么错误等。😲- 另外一个表示发生的错误类型的方法是异常(Exception)。🌝
- 同学们进入大二下,学习数据管理技术后,就更能深刻地理解这样设计实体类的意义了. 😄
3.2 继承(Inheritance)
为什么要继承?简单来说,就是降低代码重复度,并且提供统一的接口。实际上,这两个功能是相互绑定的。例如:
//Person.java
public class Person{
protected String name;
protected String getName(){
return "I am " + name;
}
}
//Teacher.java
public class Teacher extends Person{
@Override
protected String getName(){
"I am " + name + ", a teacher";
}
}
//Student.java
public class Student extends Person{
;
}
Person, Teacher, Student类都含有String name的属性,因此重复写三个String name是不必要的。这三个类也都具有getName()的方法,Student的和Person保持一致,而Teacher的则进行了一些改进。因为具有相同的属性名和方法名,当别人需要接手你的项目时,需要阅读并理解的方法数量变减少了,这将更有利于你们的协作。
因为继承在先前的实验中也进行了详细的说明,在此就不过多赘述。
3.3 多态(polymorphism)
多态是面向对象里面的大杀器。某种程度上,是否是一个优秀的面向对象编程的程序员,就在于你是否熟练掌握了多态。接下来,我们将会给出一种新的设计思路,该设计实现思路其实和设计模式中的State模式比较类似。在这种方法下,我们便可以简化逻辑,让我们的程序更加模块化,减少DEBUG的难度。
- 此处给出的样例是为了体现面向对象编程中的 设计思想, 并不推荐直接在迭代中使用。🤔
- 如果你想要使用,应该进行一些深入的思考。否则只会越用越乱。 🤔
首先我们可以定义User类为抽象类,其有一个抽象方法execute():
public abstract class User{
//其他属性,此处略去
public abstract void execute();
}
然后再分别定义Administrator, Student和Teacher类,继承User类并且实现execute()方法:
public class Administrator extends User{
@Override
public void execute() {
System.out.println("I am administrator!");
}
}
public class Student extends User{
@Override
public void execute() {
System.out.println("I am student!");
}
}
public class Teacher extends User{
@Override
public void execute() {
System.out.println("I am teacher!");
}
}
这里只是做了一个简单的实现示例。在我们的场景中,execute()方法很明显 应该与命令相关, 你可以将命令作为参数传入,也可以在User类中创建一个 命令的容器(这个方法主要可以用来进行命令的撤回,但是我们的项目并不涉及) 作为其成员属性,在执行execute()的时候只需要从该容器中取出相应的命令便可。
事实上,你还可以使用 Command模式 来解决这一类问题。另一方面, State模式 其实和Strategy模式比较类似,同学们也可以多做了解。😄
为什么要这样实现呢?其实主要是为了防止 硬编码(直接将具体的值,如字符串、数字、路径等,写入源代码中。你可以将这里的具体的值理解为一个函数) 导致的难以DEBUG的问题。试想如下的代码:
//未使用继承版本: void handleCommand(){ User user; // user代表当前用户, user.status代表身份 if(user.status == "Administrator"){ // handle command... } else if(user.status == "Student"){ // handle command... } else if(user.stauts == "Teacher"){ // handle command... } } //仅把继承作为提取公共属性和方法使用: void handleCommand(){ User user; //user代表当前用户 if(user instanceof Administrator){ //handle command... } else if(user instanceof Student){ //handle command... } else if(user instanceof Teacher){ //handle command... } }
一方面,如果我们有很多种User,每种User对应一个相同的命令都有不同的解析策略,如果这个策略很复杂,每个if块中就会有若干行代码,更可怕的是,如果这其中还有if分支,就会更加难以阅读。而且,当我们需要修改相应的处理方式时,很有可能因此犯错。
例如下面的代码 (检测平均分是否正确,并且进行学生成绩、评价和学生的配对。这并不是我们迭代的功能,只是为了示范) :
public class DEMO{ public void judge(Integer flag,String message,List<Integer> grades, > Double average, List<String> comments){ if(a == 1){ if(message.isEmpty()){ System.out.println("Message is empty."); } else{ int count1 = grades.size(); if(count == 0){ System.out.println("Grade list is empty."); } else { int count2 = comments.size(); if(count2 != count1){ System.out.println("The number of comments > doesn't equals to the number of grades."); } else{ double sum = 0; for(Integer grade : grades){ sum += grade; } if(sum / count1 != average){ System.out.println("The average is wrong."); } else{ matchGrade(grades, comments, average); // 进行成绩和评价的匹配 } } } } } else if(a == 2){ //... } } }如果行数继续增加,代码可读性将会大幅度下降。即使是代码编写者,长时间不管理这部分的代码,之后也需要花费一定的时间来进行理解。
很显然,没有人喜欢阅读一个几百行的if嵌套版块。
但是,我们如果采取了提示的写法,那么整个逻辑就变成了:
void handleCommand(){ User user; user.execute(); }
如果某一种身份(权限)处理命令是出现了BUG,我们也只是需要去到相应的实现部分对代码进行修复便可。
简单来说,其实就是将具体的执行逻辑和算法放在其他的类里面(类似于工具类),而不是全部堆在一个类里面。
3.4 真正的State模式
由于我们的迭代中,每个命令的效果其实相差不大,因而使用
State模式其实不是很合适。
进一步分析,我们发现,在User类里面添加的execute()方法似乎很奇怪,因为真正执行命令的,实际上是我们的系统,这样破坏了封装性和模块性
另外,可以看到,其实主要影响我们执行命令的,无非就是当前用户的身份(权限)。这其实就是一个状态,而我们执行命令的方式其实就和这个状态有关,所以我们完全可以把执行命令的方法封装在一个State类里面。这样,我们还是按照先前实体类的设计方式来设计Student, Teacher, Administrator类,而采用一个Executor类来执行若干命令。同时,我们有StuState, TeacherState, AdminState来分别代表三种权限下的状态。
//State.java public abstract class State{ public abstract void execute(); } //StuState.java, TeacherState, AdminState类似 public class StuState extends State{ @Override public void execute(){ System.out.println("I am a student."); } } //Executor.java public class Executor{ private State state; //表明当前的状态 public void handleCommand(){ state.execute(); } }
这样,根据不同的State,我们就可以调用不同的execute()函数,执行在3种权限下,对命令的解析。
- 你应该考虑state的切换问题。
- 一般地,我们会传入一个参数以表示解析执行的是哪个命令,但在上述实现中省略了这一点
- 细心的同学可能已经发现,
StuState, TeacherState, AdminState可以使用Singleton模式(单例模式)。