Hint

1. 命令优先级

首先,输入命令未定义时,输出命令不存在,替换下面的 cmd 为具体的命令名称。

Command 'cmd' not found

例如输入 logged out,由于命令 logged 未定义(注意:这里的out作为参数而不是命令的一部分出现),所以输出

Command 'logged' not found

其次,当输入命令有定义但参数个数不合法时,输出

Illegal argument count

当命令有定义,参数个数正确时,才会进一步输出 Already logged inBye ~ 等成功或失败信息。

当一句命令存在多种非法情况,按上述顺序,只输出最先发生的非法信息。

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 等的使用

在开发时, 合理使用ListMap加快我们的开发效率。

对于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  
    }  
}

更多的使用方法同学们可以查阅相关资料,这里就不展开了。ListMap还涉及到“泛型”,感兴趣的同学可以提前了解了解。实际上,在我们给出的实际中,List<TargetClass>Map<String, Person>的尖括号的使用,其实就对应了泛型。

可以看到,在使用ListMap的时候,尖括号会传入一个类。因为Java面向对象的特性,基本数据类型(int,double,boolean)是不可以传入的,必须传入一个类。而每个基本数据类型都会对应一个类,例如int对应Integer,所以同学们在使用到ListMap,需要传入基本数据类型的时候,要记得把基本数据类型换为其对应的类。

List 和 Map 的区别:
  • List:它是一个有序集合,可以包含重复的元素。它主要用于存储一系列顺序排列的对象。
  • Map:它是一个键值对集合,用于存储键和值之间的映射,其中键是唯一的,而值可以重复。
HashMap 与 TreeMap 的区别:

HashMapTreeMap都是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时,你可以在同项目下的任何一个文件,任何一个类中访问并使用那个属性,岂不快哉?

但是,随着类的数量的增多,这样做的麻烦逐渐显现:

  1. 类与类之间无封装可言,大家都是透明的,可以肆意访问并修改各自的属性,这就会导致混乱,而且对属性的访问和修改也不是很明显(比较a = 1 , setA(1)),这会增大DEBUG难度
  2. 一般来说,我们只会提供能够完成一个方法的最少但是必要的信息。例如,现在有一个方法需要知道当前有多少用户(假定有一个List用于存储用户),那么我们只需要传入这个List的长度,而不需要将整个List都传入,然后再求长度。一方面,这样可能会造成不必要的内存开销(值传递和引用传递的问题,虽然在Java中这类问题基本不存在),另一方面,传入List后,这个方法就可以修改其内容,这可能会导致不必要的麻烦
  3. 难以进行合法性判断。如果在访问和修改某个属性时,需要考虑到类似于权限和值的合法性问题,我们总是会写一个专门的合法性判断方法,甚至创造一个合法性判断类。但是这显然是没必要的,一个类的属性的合法性判断工作没有任何理由交给其他的类担任,相关的工作我们完全可以在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;  
        }  
    }  
}
  1. setter()方法的返回值不一定非要是void,你可以用返回值来表示执行的状态,发生了什么错误等。😲
  2. 另外一个表示发生的错误类型的方法是异常(Exception)。🌝
  3. 同学们进入大二下,学习数据管理技术后,就更能深刻地理解这样设计实体类的意义了. 😄

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的难度

  1. 此处给出的样例是为了体现面向对象编程中的 设计思想, 并不推荐直接在迭代中使用。🤔
  2. 如果你想要使用,应该进行一些深入的思考。否则只会越用越乱。 🤔

首先我们可以定义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种权限下,对命令的解析。

  1. 你应该考虑state的切换问题。
  2. 一般地,我们会传入一个参数以表示解析执行的是哪个命令,但在上述实现中省略了这一点
  3. 细心的同学可能已经发现,StuState, TeacherState, AdminState可以使用Singleton模式(单例模式)。
Built with MDFriday ❤️