简述Java实验设计的实践过程

本来,这次实验应该写一个实验报告,描述在实验过程当中遇到的一些问题和解决方案。不过最近有很多同学都在纠结这次实验应该从哪下手,我从一个Java新手的角度帮大家理清一下思路,就当做是实验报告了。(注:由于Java水平有限,下文中的很多习惯用词可能来自Objective-C语言。)

我的设计模式灵感来源于著名的MVC模式,有兴趣的同学可以百度一下。

具体代码可以参见GitHub

一、基本结构

说是学生管理系统,就应该可以预见到,主界面应该是一个有管理功能的表格,按照常理,我们可以添加/修改/删除学生的信息。同时,一个功能完备的系统一定不是随意进入的,所以我们还需要登录模块。登录模块一般包括登陆/注册/忘记密码三部分,这三部分的注册和忘记密码在样式上基本相同,都是用户名占一行,密码和确认密码占两行。

构思好基本的界面之后,我们很有必要把它画下来,方便我们在写代码的时候理清层次。

二、数据模块
1.学生

数据模块的搭建是贯穿整个项目的。第一次的搭建很可能和项目完成的时候相差甚远。我一开始搭建数据模块的时候,自以为方法已经给的尽可能的全,但是标记第一个release的时候,和最初相比多了一倍的代码量。这里,我们强调的是代码在实现某些功能的时候,需要根据需求不断地更新基础需求。

由于我们在管理的是学生的信息,所以一开始需要写学生类。学生类应该包含一些简单的属性,你也可以根据需要在后期添加,同时写好所有属性的get/set方法,一定要注意类型。

到这里,看看表,过去了2分钟。在这2分钟里,你完成了这个项目的第一步,同时也是最关键的一步——学生类都搭建好了,管理学生还不简单吗?

但是,如果你管理的只是三四个学生,就像导师带研究生一样,你还可以简单地实例化三四个学生对象就可以了,非常可惜的是,学生管理经常出现大数据——如果你是班主任,你要管五六十个学生,你要是辅导员,就得管几百个学生。一个个实例化显然是不现实的。所以,我们可以选择构建一个线性链表管理学生。

阅读笔记:线性链表是一种具有连接存储结构的线性表。相邻的元素在逻辑和存储地址上可能都并不是相邻的。简单地说,它就是线性结构的“链表”。

读懂上面的内容,就相当于为你在数据结构的学习上做了一点点预习。当然,数据结构并不是这么简单的,线性结构(linear structure)是其中不那么抽象的一部分。考虑到我们管理学生的时候一般并不考虑学生之间的关系,只是在序上有所区别,按序排列,所以学生可以添加到一个线性列表里。

2.学生列表

现在,整个项目最复杂的类之一即将开始——你的好友【学生列表】上线了。

学生列表是在项目中的主体功能中最重要的一环。幸好,Java在表面上并没有C语言那么复杂的指针,线性链表其实只是一个泛型的数组。如果你的记忆力还好,就会想起实验三中有一个重要的类叫ArrayList,就是你了。

学生列表的类里,方法会有很多。比如添加/修改/删除某个学生,甚至是某几个学生。获取学生的列表,查找列表当中的某个学生等等。需要注意的是,一定要为每个方法都取一个容易识别的名字,名字不怕长,只怕你以后再也看不懂了。比如添加学生的方法,添加某个学生你可能会写成addStudent(ZHRMoeStudent),那么添加很多学生应该取什么名字呢?好的方法是,添加一个学生的时候,方法名就说好添加一个学生,例如addSingleStudent(ZHRMoeStudent)。(这里的ZHRMoeStudent只是我假设的学生类的名字)

当你用正常的方法声明学生列表的实例时,依然会像上次一样收到一个提示,泛型的具体类型并没有声明。这次,我们不再使用屏蔽的措施,而是正面地解决它。我们可以用如下的方式声明一个学生列表实例:
public ArrayList<ZHRMoeStudent> studentList = new ArrayList<ZHRMoeStudent>();

如果你只用了3分钟就写好了这个类,我首先要恭喜你有一个飞快的手速,但同时,我也建议你再仔细地想想有没有什么可以添加的方法,免得以后添加的时候打乱编程思路。

3.用户

登陆的时候,你想判断用户名密码是否正确,应该去找谁呢?答案就是User类。所谓的用户其实就是用这个用户系统的人,他们的属性就是用户名、密码。写好他们对应的get/set方法,看看表,2分钟。

4.用户列表

注册?忘记密码?如果你想更改用户的信息,要是只实例化了几个用户的对象,肯定是说不过去的。这样的话,我们就应该再给用户建立一个列表。方法和学生的列表类似,都是删除/添加等等。这个过程大概需要5分钟的时间

5.小结

理论上讲,数据模块的搭建只需要15分钟。虽然你之后肯定还需要再雕琢这一部分的方法,而且这种修改一定是贯穿整个项目周期始终的。但是,迈出了这重要的第一步,意义是非常重大的。还需要额外说明的是,学生列表和用户列表的两个列表属性一定是静态的,不要问我静静是谁。

三、登陆模块
1.登陆界面

终于可以画界面了!

为了让你可以更加自由地描绘界面结构,我只提供一些简单的思路。

在画界面之前,一定先在哪里画出这个界面的具体样子,这对你了解这个界面的结构和排列有非常大的帮助。然后,分区域把对应的元素规划成JPanel类的对象。

由于,登陆界面这个类是一个图形界面,所以它应该继承于JFrame

需要注意的是,密码框并不是简单的JTextField,而是JPasswordField,这种框当中的文字都会(在表面上)加密成点。

可能用到的界面元素类:

  • 区块 JPanel
  • 文字 JLabel
  • 按钮 JButton
  • 输入框 JTextField
  • 密码框 JPasswordField

同时,界面上的按钮在点击的时候应该做出反应,这时候,我们应该为每一个按钮都添加事件监听,监听的是这个界面本身。所以相应的语句应该是someButton.addActionListener(this)

然后就报错了。

因为,这个界面本身并不是一个动作的监听者,它只是一个界面而已。所以,我们让它实现ActionListener这个接口。相应的类定义是:

public class ZHRMoeLogin extends JFrame implements ActionListener {
    //这个界面的代码
}

报错变成了提示,这个提示告诉我们,接口的方法我们还没有实现。所以接下来,我们要给这些按钮实现动作事件。

public void actionPerformed(ActionEvent a) {
    if(a.getSource() == someButton) {
        //写关于这个的按钮按下时的代码
    } else if(a.getSource() == anotherButton) {
        //写关于另一个按钮按下时的代码
    }
}

需要解释的是,ActionEvent是一个动作事件,其实也就是点击了这个界面的某个元素,根据点击不同的按钮,我们可以做出不同的反应。

另一个问题是,用户名和密码需要和之前的某个具体的用户相同,这里我不详细介绍对照的方法,不过搜索一定是一个不错的选择。另外,用户名和密码为空的时候,登陆应该被阻止(一开始,我故意留下了这个漏洞方便其他界面的检查,所以是否在一开始就解决这个问题,依情况而定)。这其中的逻辑,你们可以用离散数学的分析方法做分析(顿时就高大上了)。

2.注册界面

注册需要对用户的列表进行添加操作,这里就要看你给用户列表写的方法是否详尽了。

同时,注册新用户的时候,需要输入两遍密码防止用户手残。两次密码需要对照一致才能注册,而用户名不可以是已经注册了的用户名,也要做相应的检查。这其中的逻辑,也应该细致地考虑。

3.忘记密码界面

忘记密码界面其实就是修改密码的界面。这个界面的结构和注册界面大体相似,但是不同在于,只有用户名已经存在的时候才能更改密码(即逻辑与注册界面相反)。而用户列表内的操作,可以选择先删除原来的用户,然后把用户名和新的密码重新生成一个用户加入列表当中。

四、主界面
1.主界面概述

为了让我们写出的主界面美观,代码应该下一番功夫认真排版。

主要的数据都填在了一张JTable的对象(也就是一张表)里,这张表由于需要滚动,所以要加入JScrollPane,即滚动的模块当中。

JTable数据的获取,需要通过setModel方法设置。

studentListTable.setModel(new TableModel() {
            @Override
            public int getRowCount() {
                return ...;
                //这里需要返回列表的行数
            }

            @Override
            public int getColumnCount() {
                return ...;
                //这里需要返回列表的列数
            }

            @Override
            public String getColumnName(int columnIndex) {
                return new String[]{"", "", ...}[columnIndex];
                //这里需要返回列标题的序列
            }

            @Override
            public Class<?> getColumnClass(int columnIndex) {
                return String.class;
                //这里需要返回类型
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return true;
                //这里返回的是列表的内容是否可以修改,这里设置为是,就可以完成学生信息的修改了
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                ZHRMoeStudent s = studentArray.getSingleStudent(rowIndex);
                return new Object[]{...}[columnIndex];
                //这里返回学生信息,填满列表 需要填写get方法
            }

            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                ZHRMoeStudent s = studentArray.getSingleStudent(rowIndex);
                switch (columnIndex) {
                    case 0:
                        ... //填写第0列的set方法,以下依此类推
                        break;
                    case 1:
                        ...
                        break;
                    case 2:
                        ...
                        break;
                    case 3:
                        ...
                        break;
                    case 4:
                        ...
                        break;
                    case 5:
                        ...
                    default:
                        break;
                }
                //修改列表元素
            }

            @Override
            public void addTableModelListener(TableModelListener l) {
                //重写方法
            }

            @Override
            public void removeTableModelListener(TableModelListener l) {
                //重写方法
            }
        });

通过这些代码,列表就能够得到数据了。

2.添加学生界面

添加学生的界面应该是可以获取学生的所有属性的。注意,有些属性例如“性别”,让用户去输入显然是可能出现“手残”的情况的。为了避免这种笑话的发生,我们可以添加两个JRadioButton分别表示男女,然后把两个选项放进一个ButtonGroup里,使得这两个选项只能单选。

这两个JRadioButton也应该是动作的监听者,用来设置性别的男女。

3.总结

图形界面是不是不好画啊,是不是要写好多东西啊?

可是,用代码写界面的确是这么复杂的,但通过一些字符就可以绘画出一个有意义的界面出来,的确非常有成就感。

四、项目维护

总的来说,到这里我们的项目中的基本功能已经实现了。但是很有可能出现某些不可预料的奇怪情况,bug。遇到bug不要着急,这是提升我们能力的好机会。好在这个项目的数据规模并不复杂,bug的原因经常显而易见。如果真的遇到了不可解决的bug,那么在实现了所有基本功能的前提下,舍弃一些极难实现的功能,也是情有可原的。否则,高端的程序员存在的意义是什么呢?

在项目过程当中遇到问题,可以找我,也可以找其他在Java方面更有研究的同学解决,希望大家可以深刻地理解设计的步骤,对未来设计更大的项目有所帮助。

五、源代码

最新Pre-Release V1.1 代码下载

*Warning:本项目源代码公开,但是项目组暂时不同意代码开源使用,使用前请和我们取得联系。

2015.4.17 C2613次列车上 一稿

2015.4.19 C2076次列车上 二稿

ZHRMoe Studio, 2015.

结合实验浅谈ArrayList用法

描述

ArrayListJava中的一种动态数组,相关的问题出现在Java实验三第5题(选做)。

改写程序清单10-6中的Course类。使用ArrayList代替数组来储存学生。不应该改变Course类的原始合约(即构造方法和方法的定义都不应该改变)。

程序清单10-6如下所示:

public class Course {

    private String courseName;
    private String[] students = new String[100];
    private int numberOfStudents;

    public Course(String courseName) {
        this.courseName = courseName;
    }

    public void addStudent(String student) {
        students[numberOfStudents] = student;
        numberOfStudents++;
    }

    public String[] getStudents() {
        return students;
    }

    public int getNumberOfStudents() {
        return numberOfStudents;
    }

    public String getCourseName() {
        return courseName;
    }

    public void dropStudent(String student) {
        //Left as an exercise in Exercise 10.9
    }
}
分析

既然需要用到ArrayList类,我们就要考察一下这个类当中有哪些需要我们关注的方法。首先我们需要初始化一个动态数组students,即构造方法。在后面的addStudent(String)dropStudent(String)两个方法中,我们分别需要在动态数组中添加和删去指定的元素。

打开ArrayList.java文件可以寻找我们需要的方法。

  • 构造

ArrayList类提供了3种构造方法,分别是:

  1. ArrayList(int initialCapacity)
  2. ArrayList()
  3. ArrayList(Collection<? extends E> c)

如果你看不懂第三个,暂时没有关系,一会你会对它有所了解的。

  • 添加

ArrayList类提供了4种添加方法,但是其中2种是布尔型的判断,只有2种可以添加元素:

  1. add(int index, E element)
  2. add(E e)

传入的是一个整形值和一个任意类型的元素或者只有一个元素(关于E类型你可以暂时这样理解)。

  • 删去

ArrayList类提供了4种删去方法,但是和添加方法一样,只有2种可以删去元素:

  1. remove(int index)
  2. remove(Object o)

同上,传入一个任意类型的元素或者是角标号。

代码及解释

根据题目要求,我们将使用第一种构造方法。新的Course类代码如下:

public class Course {

    private String courseName;
    private ArrayList students = new ArrayList(100);
    private int numberOfStudents;

    public Course(String courseName) {
        this.courseName = courseName;
    }

    public void addStudent(String student) {
        students.add(numberOfStudents, student);
        numberOfStudents++;
    }

    public ArrayList getStudents() {
        return students;
    }

    public int getNumberOfStudents() {
        return numberOfStudents;
    }

    public String getCourseName() {
        return courseName;
    }

    public void dropStudent(String student) {
        students.remove(student);
        numberOfStudents--;
    }

}

可以看到,由于添加和删除方法的限制,我们使用的都是传入元素的添加删除方法。

不过,在某些IDE当中,这些代码可能会带来一条Warning:

Unchecked call to add(int, E) as a member of raw type 'java.util.ArrayList'.

根据字面意思可以知道,是泛型类的赋值需要检测的原因(你也可以认为是类型可能会发生不匹配)。但是作为一个程序员(强迫症),一定要把Warning扼杀在摇篮里。你可以在类的声明之前添加一行代码去掉这条提示:

@SuppressWarnings("unchecked")

测试方法的代码如下:

public class CourseTest {

    public static void main(String[] args) {
        Course course = new Course("Java");
        for (int i = 1; i <= 10; ++i) {
            course.addStudent(String.format("Student%d", i));
        }
        System.out.println("Step 1:We initialized 10 students into the ArrayList.");
        System.out.println("Course Name: " + course.getCourseName() + "\nNumber of students: " + course.getNumberOfStudents() + "\nStudents List: " + course.getStudents());
        for (int i = 1; i <= 5; ++i) {
            course.dropStudent(String.format("Student%d",i));
        }
        System.out.println("\nStep 2:We removed 5 students from the ArrayList.");
        System.out.println("Course Name: " + course.getCourseName() + "\nNumber of students: " + course.getNumberOfStudents() + "\nStudents List: " + course.getStudents());
    }
}

这其中需要解释的是,String.formatString类的自带方法,意思是按照某种格式生成字符串,由于我加入了循环,生成的学生名就是Student1到Student10了。

吐槽:OC在写这个方法的时候就比较复杂了,大概是:

[NSString stringWithFormat:@"blabla"];

总结

这道题作为选作还是很有必要的,我大概看文件就要花了半个小时的时间,确实是个很考验能力的题目呢。

ZHRMoe Studio 4/10/2015.

ParseInt方法的实现

描述

这个问题来自1到4班Java第二次实验第2题,周围有好多同学一看到需求比较迷茫,所以写了这个短文(卖萌)。

  • 静态方法parseInt(char[])将数字字符构成的数组转换为一个int值。
  • 静态方法parseInt(String)将字符串转换为一个int值。
分析

先说Stringint,我们不能把一个String分开来单独处理每一位(而char[]就能轻易地做到),所以,需要调用String的方法toCharArray()处理。

那么对于一个char[]来说,我们需要逆序读取这个线性列表的每一个元素,然后考虑位上的权值,一股脑地加进需要求得的总和。

例如'12345',是由1 * 10000 + 2 * 1000 + 3 * 100 + 4 * 10 + 5 * 1组成的。考虑到这里,我们需要两个计数器solve求得结果以及right计算权值。

另外,由于是倒序处理,我们还要知道数组的长度也就是array.length,但是如果你用这个数直接循环,会得到栈溢出的错误。(错误提示如下)

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: **
at MyInteger.parseInt(MyInteger.java:**)
at MyInteger.main(MyInteger.java:**)

这是因为,角标都是从0数的嘛,所以你要用长度减1。(高手请忽略我上面几行的卖ke萌pu)

代码及解释

所以,parseInt(char[])的实现如下:

 Java Code 
public static int parseInt(char[] array) {

    
int right = 1;

    
int solve = 0;

    
for (int i = array.length - 1; i >= 0; --i) {

        solve = solve + right * ((
int)array[i] - 48);

        right *= 
10;

    }

    
return solve;

}


你可能注意到,(int)array[i]减了48,至于这是为什么,你可以把减48去掉,然后输出结果查看一下。

parseInt(String)的实现,可以用已经写好的上面的方法偷懒:

 Java Code 
public static int parseInt(String str) {

    
char[] array = str.toCharArray();

    
return parseInt(array);

}

总结

其实我从来没有写过这么科普向的解题报告,所以Java的实验真的很水,很水……

ZHRMoe Studio 4/8/2015.