所看教程(视频):《浙江大学-翁恺-Java-面向对象程序设计》
作为我自己的复习笔记,也可以当做该视频的同步笔记
上接JAVA/面向对象学习笔记(2)

Swing

Swing是一个为Java设计的GUI工具包,是java的基础类(import javax.swing.;)
在Swing中,所有我们在界面中看到的东西都是*部件
(组件)

其中容器是一种特殊的部件
部件可以被放进容器中,当然容器也能放进容器中

Swing提供了一个底层容器类JFrame,即整个窗口

JFrame中常用的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//创建一个无标题的窗口
JFrame()
//创建标题为s的窗口
JFrame(String s)
//设置窗口的初始位置是(a,b),即距屏幕左面a个像素,距屏幕上方b个像素,窗口的宽是width,高是height。
public void setBounds(int a,int b,int width,int height)
//设置窗口的大小。
public void setSize(int width,int height)
//设置窗口的位置,默认位置是(0,0)。
public void setLocation(int x,int y)
//设置窗口是否可见,窗口默认是不可见的。
public void setVisible(boolean b)
//设置窗口是否可调整大小,默认可调整大小。public voiddispose()撤销当前窗口,并释放当前窗口所使用的资源。
public void setResizable(boolean b)
//撤销当前窗口,并释放当前窗口所使用的全部资源
public void dispose()
//设置窗口的扩展状态
public void setExtendedState(int state)
//其中参数state取JFrame类中的下列类常量:
MAXIMIZED_HORIZ (水平方向最大化),
MAXIMIZED_VERT (垂直方向最大化),
MAXIMIZED_BOTH (水平、垂直方向都最大化)。
//该方法用来设置单击窗体右上角的关闭图标后,程序会做出怎样的处理,
public void setDefaultCloseOperation(int operation)
//其中的参数operation取JFrame类中的下列int型static常量,程序根据参数operation取值做出不同的处理:
DO_NOTHING_ON_CLOSE(什么也不做),
HIDE_ON_CLOSE (隐藏当前窗口),
DISPOSE_ON_CLOSE (隐藏当前窗口,并释放窗体占有的其他资源),
EXIT_ON_CLOSE (结束窗口所在的应用程序)

add

通过add把一个部件加到一个容器中
部件被加到容器后,就受这个容器所管理
容器管理部件的方式叫布局管理器
JFrame默认采用的布局管理器叫BorderLayout,默认把部件放到CENTER

1
2
3
4
5
theView = new View(theField);//theView是一个容器
JFrame frame = new JFrame();//创建一个底层容器
frame.add(theView);//把theView容器加到底层容器中。默认为中间
JButton btnstep =new JButton("单步");//btnstep是一个按钮部件
frame.add(btnstep, BorderLayout.SOUTH);//把btnstep部件加到底层容器中,且放到南边(窗口最下面)

BorderLayout

BorderLayout把整个容器划分为五个部分

后面放进去的部件会替换掉相同位置的部件(这就是为什么之前界面中只剩下一个按钮了)
当有部分没有部件时,其他部分会膨胀,将那个位置所占据

BorderLayout会根据部件里面的东西来帮我们计算,这个部件需要占据多大的空间

消息机制

现在我们有了一个按下去没反应的按钮
如何让按钮按下去有反应?程序如何知道按钮被按下去了?
用户在图形界面做了一些操作,通过一些路径让程序知道,这个路径叫做消息机制

Java的Swing类实现了一个有意思的消息机制

1
2
3
4
5
6
7
8
JButton btnstep =new JButton("单步");
frame.add(btnstep, BorderLayout.SOUTH);
btnstep.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("成功按下!");
}
});

运行一下,当我们点击一次按钮,控制台都会输出一次“成功按下!”

现在程序已经知道按钮被按下,且在上面的古怪代码中能成功做一些我们期望程序做的事(输出点东西)
我们可以把输出点东西换成其它事情,在狐狸和兔子中,step()函数控制单步
我们只需要做下面一些动作,就能让按钮控制单步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private JFrame frame;//把frame从FoxAndRabbit()中拿出来,让它变为FoxAndRabbit类中的成员变量
···
//JFrame frame = new JFrame();
frame = new JFrame();
···
btnstep.addActionListener(new ActionListener(){
//实现了ActionListener这个接口的匿名类
@Override
public void actionPerformed(ActionEvent e) {
step();
frame.repaint();
}
});
···
//fab.start(500);把main里的这句去掉,不让程序主动地开始

现在每按一次按钮,程序就会运行一步

按钮自己有代码,知道自己被按下去了,但按钮作为一个早已经定好的类不可能有代码去调用step()
但实际效果就是,每按一次按钮,step()就会被调用一次,这是怎么做到的?

JButton类提供了一个接口,只要实现了这个接口的类的对象,都可以通过addActionListener()方法注册给JButton,当按钮发现自己被按下去了,就会检查有没有东西注册在按钮那,接着找到重写的actionPerformed(),这样JButton就知道step()了
注册进去的东西,是运行时候一个动态的对象

这就是反转控制(Swing的消息机制):
·由按钮公布一个守听者接口和一对注册/注销函数
·你的代码实现那个接口,将守听者对象注册在按钮上
·一旦按钮被按下,就会反过来调用你的守听者对象的某个函数

内部类、匿名类

刚刚实现接口的代码看起来十分奇怪

1
2
3
4
5
6
7
8
btnstep.addActionListener(new ActionListener(){
//实现了ActionListener这个接口的匿名类
@Override
public void actionPerformed(ActionEvent e) {
step();
frame.repaint();
}
});

可以换种写法
1
2
3
4
5
6
7
8
9
10
11
//在类中新增这个类
private class stepListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
step();
frame.repaint();
}
}
···
//原来的代码替换为这句
btnstep.addActionListener(new stepListener());

在一个类的内部,再定义一个类,这个类就叫内部类
内部类可以直接访问其所处类的所有成员
java的内部类也是类的成员
外部是函数时,只能访问那个函数里final的变量

用匿名类实现接口

1
2
3
4
5
6
7
8
new ActionListener(){
//实现了ActionListener这个接口的匿名类
@Override
public void actionPerformed(ActionEvent e) {
step();
frame.repaint();
}
}

new对象的时候给出的类的定义形成了匿名类
匿名类可以继承某类,也可以实现某接口
Swingl的消息机制广泛使用匿名类
外部是函数时,只能访问那个函数里final的变量

为什么需要匿名类?
Swing的消息机制决定了,每个部件发出的消息,都需要新的类去实现接口,然后去接收消息,当部件很多时,给每个类起名字非常麻烦


一个课程表

做一个课程表程序,它有8行7列,有表头表示7天,每个格子用户能自己编辑内容
效果是这样:

有前面Swing的基础,我们知道想要有一个窗口,需要用到JFrame类,来创建一个底层窗口

KCB.java
1
2
3
4
5
6
7
8
9
10
11
12
package kcb;

import javax.swing.*;

public class KCB {

public static void main(String[] args) {
JFrame frame = new JFrame();//声明一个窗口
frame.pack();//自动调整窗口大小
frame.setVisible(true);//显示窗口
}
}

现在运行,只有一个空空的窗口,一个空空的容器,我们需要往里面放部件
想要一个表格,那就放一个表格进去
KCB.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package kcb;

import javax.swing.*;

public class KCB {

public static void main(String[] args) {
JFrame frame = new JFrame();//声明一个窗口
JTable table = new JTable());//声明一个表格
frame.add(table);//把表格放进去
frame.pack();//自动调整窗口大小
frame.setVisible(true);//显示窗口
}
}

运行一下,还是啥都没有,因为我们还没初始化表格,还没给表格它要的数据

用JTable类可以以表格的形式显示和编辑数据。
JTable类的对象并不存储数据,它只是数据的表现。
JTable实现了数据与表现的分离

新建一个KCBData类,作为表格的数据
让这个类实现一个叫TableModel的接口

KCBData.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package kcb;

import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

public class KCBData implements TableModel {

@Override
public int getRowCount() {
return 0;
}

@Override
public int getColumnCount() {
return 0;
}

@Override
public String getColumnName(int columnIndex) {
return null;
}

@Override
public Class<?> getColumnClass(int columnIndex) {
return null;
}

@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return false;
}

@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return null;
}

@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {

}

@Override
public void addTableModelListener(TableModelListener l) {

}

@Override
public void removeTableModelListener(TableModelListener l) {

}
}

TableModel接口是由JTable提供给我们的
TableModel告诉我们,只要实现了它,就能作为数据交给JTable

完善一下KCBData

KCBData.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package kcb;

import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

public class KCBData implements TableModel {

private String[] title = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"};//表格标题
private String[][] data = new String[8][7];//真正放7天8节课的数据结构
//这个data数组里面都是String类型的管理者,所以需要初始化每个管理者去管理一个String类型的空数据
public KCBData() {
//构造方法,初始化数据,每一行的数据都是空的,即没有数据,这样才能显示表格,否则会报错
for (int i = 0; i < data.length; i++) {
for (int j = 0; j < data[i].length; j++) {
data[i][j] = "";//让每一个单元格都是空的
}
}
}
@Override
public int getRowCount() {
return 8;//表格有8行
}

@Override
public int getColumnCount() {
return 7;//表格有7列
}

@Override
public String getColumnName(int columnIndex) {
return title[columnIndex];//返回一个表头
}

@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;//告诉表格每列的数据类型,每一个都是String类型
}

@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return true;//每个单元格都可以编辑
}

@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return data[rowIndex][columnIndex];//将每一个单元格的数据返回,让表格拿到
}

@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
data[rowIndex][columnIndex] = (String) aValue;//将每一个单元格的数据设置为aValue,即用户输入的数据
}

@Override
public void addTableModelListener(TableModelListener l) {
//添加监听器
}

@Override
public void removeTableModelListener(TableModelListener l) {
//移除监听器
}
}


现在表格能拿到数据了,它知道该怎么画一个表格
但运行一下,还是没有表头

这是因为JTable组件显示数据时,如果直接将其放置在Frame的contentPane中则表头一行会显示不出来,如果将其放置在JScrollPane中显示数据的话,表头会自动显示出来。
暂时无需关心为什么

再完善下KCB类,一个课程表就完成了

KCB.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package kcb;

import javax.swing.*;

public class KCB {

public static void main(String[] args) {
JFrame frame = new JFrame();//声明一个窗口
JTable table = new JTable(new KCBData());//声明一个表格
JScrollPane pane = new JScrollPane(table);//声明一个滚动面板
frame.add(pane);//将滚动面板添加到窗口
frame.pack();//自动调整窗口大小
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//设置窗口关闭方式
frame.setVisible(true);//显示窗口
}

}

MVC设计模式

刚刚的课程表结构是这样的

当JTable决定显示多少列时会调用getColumnCount,当它要显示表头时会调用getColumnName
程序运行过程中JTable反过来调用我们自己的KCBData里的方法

数据由我们自己实现了TableModel的对象来维护,JTable只管表现不管数据

MVC:数据、表现和控制三者分离,各负其责
·M=Model(模型)
·V=View(表现)
·C=Control(控制)

模型:保存和维护数据,提供接口让外部修改数据,通知表现需要刷新
表现:从模型获得数据,根据数据画出表现
控制:从用户得到输入,根据输入调整数据

不是由接收到用户输入的代码去修改界面上的显示,而是去修改内部的数据,内部的数据去触发界面的更新

这样做的好处:每一部分都很单纯,尤其是View表现,只管拿到想要的数据去表现,至于数据是怎么更新,怎么生成的,它统统不管

在代码实现中,View和Control通常在同个表达界面的类中实现,因为表现和用户控制都是在界面中完成的,这和MVC并不矛盾,只是在具体实现MVC模式时的技巧


Exception异常

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;如果你用System.out.println(11/0),那么你是因为你用0做了除数,会抛出 java.lang.ArithmeticException 的异常。

写出下面的程序,idea的编辑器不会指出数组越界的错误,但运行程序控制台会抛出异常

ArrayIndex.java
1
2
3
4
5
6
7
public class ArrayIndex {
public static void main(String[] args) {
int[] a = new int[10];
a[10] = 10;
System.out.println("hello");
}
}

异常
1
2
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10
at ArrayIndex.main(ArrayIndex.java:4)

在main里面有Exception异常,问题出在在ArrayIndex的第四行,问题叫做ArrayIndexOutOfBoundsException,即数组越界

两种类型的异常与错误

检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

捕获异常

当代码某一处可能出现问题、可能出现异常,就可以将这块代码放在一个用于捕捉异常的代码块中

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
// 可能出现异常的程序代码
}catch(ExceptionName e1)//可以多个catch
{
//处理异常的代码
}catch(ExceptionName e2)
{
//处理异常的代码
}finally{
//无论是否发生异常,finally 代码块中的代码总会被执行。
// 程序代码
}

使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。try/catch代码块中的代码称为保护代码

Catch 语句包含要捕获异常类型的声明。当保护代码块中发生一个异常时,try 后面的 catch 块就会被检查。
如果发生的异常包含在 catch 块中,异常会被传递到该 catch 块,这和传递一个参数到方法是一样.

将刚刚数组越界的代码用捕捉异常处理

ArrayIndex.java
1
2
3
4
5
6
7
8
9
10
11
public class ArrayIndex {
public static void main(String[] args) {
int[] a = new int[10];
try{
a[10] = 10;
System.out.println("hello");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("error");
}
}
}

运行一下
1
error

异常是程序运行过程中可能出现的问题,现在这个代码是一定会出错的,我们改造一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Scanner;

public class ArrayIndex {
public static void main(String[] args) {
int[] a = new int[10];
int idx = 0;
Scanner in = new Scanner(System.in);
idx = in.nextInt();
try{
a[idx] = 10;
System.out.println("hello");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("error");
}
}
}

输出
1
2
3
4
输入:2
hello
输入:12
error

异常处理机制

把可能发出异常的代码放到try里,在try后面用catch去匹配可能出现的异常类型。
当try里的代码没有异常,catch里的代码不会被运行,当try里的代码出现异常,try里后续的代码都不会被执行,会直接调到catch里,在catch里处理完异常,会继续往下运行整个程序,而不会回到try。
当匹配到一个catch之后,异常就已经被处理完了,不会再去匹配另一个异常。

运行下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ArrayIndex {

public static void f(){
int[] a = new int[10];
a[10] = 10;
System.out.println("hello");
}
public static void main(String[] args) {
try{
f();
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("error");
}
System.out.println("main");
}
}

输出
1
2
error
main

当给数组赋值出现异常后,f方法后面的代码都不会被执行,然后回到调用f方法的地方,try会捕捉到f方法的异常,然后传递给catch

当有异常被抛出时,可以遵循下面的图来判断该在哪个地方处理这个异常

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ArrayIndex {

public static void f(){
int[] a = new int[10];
a[10] = 10;//抛出ArrayIndexOutOfBoundsException异常
System.out.println("hello");//异常后面的代码不会被执行
}

public static void g(){
f();//f方法抛出异常,异常没有try捕捉,所处是函数,返回调用者
}

public static void h(){
int i = 10;
if(i < 100){
g();//所处不是函数,跳出一层
}//异常没有try捕捉,所处是函数,返回调用者
}

public static void k(){
try{
h();//有try捕捉异常
}catch(NullPointerException e){//没有对应catch匹配,退出到外层
System.out.println("k error");
}//所处是函数
//返回调用者
}

public static void main(String[] args) {
try{
k();//有try捕捉异常
}catch(ArrayIndexOutOfBoundsException e){//有对应catch匹配
System.out.println("error");//处理异常
}
System.out.println("main");
}
}
输出
1
2
error
main

Java 内置异常类

异常 描述
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数”除以零”时,抛出此类的一个实例
ArrayIndexOutOfBoundsException 用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数
IllegalMonitorStateException 抛出的异常表明某一线程已经试图等待对象的监视器,或者试图通知其他正在等待对象的监视器而本身没有指定监视器的线程
IllegalStateException 在非法或不适当的时间调用方法时产生的信号。换句话说,即 Java 环境或 Java 应用程序没有处于请求操作所要求的适当状态下
IllegalThreadStateException 线程没有处于请求操作所要求的适当状态时抛出的异常
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常
NullPointerException 当应用程序试图在需要对象的地方使用 null 时,抛出该异常
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯
StringIndexOutOfBoundsException 此异常由 String 方法抛出,指示索引或者为负,或者超出字符串的大小
UnsupportedOperationException 当不支持请求的操作时,抛出该异常
下面是Java 定义在 java.lang 包中的检查性异常类:
ClassNotFoundException 应用程序试图加载类时,找不到相应的类,抛出该异常
CloneNotSupportedException 当调用 Object 类中的 clone 方法克隆对象,但该对象的类无法实现 Cloneable 接口时,抛出该异常
IllegalAccessException 拒绝访问一个类的时候,抛出该异常
InstantiationException 当试图使用 Class 类中的 newInstance 方法创建一个类的实例,而指定的类对象因为是一个接口或是一个抽象类而无法实例化时,抛出该异常
InterruptedException 一个线程被另一个线程中断,抛出该异常
NoSuchFieldException 请求的变量不存在
NoSuchMethodException 请求的方法不存在

异常方法

当catch匹配到了异常,实际上是拿到了一个异常类型的对象,我们可以让对象做事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class a {
public static void main(String[] args) {
int[] a = new int[10];
try{
a[10] = 20;
System.out.println("hello");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("error");
System.out.println(e.getMessage());
System.out.println();
System.out.println(e);
System.out.println();
e.printStackTrace();
}
}
}

输出
1
2
3
4
5
6
7
8
error
Index 10 out of bounds for length 10

java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10

java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10
at a.main(a.java:5)

异常方法 描述
String getMessage() 返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了
Throwable getCause() 返回一个 Throwable 对象代表异常原因
String toString() 返回此 Throwable 的简短描述
void printStackTrace() 将此 Throwable 及其回溯打印到标准错误流
StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底
Throwable fillInStackTrace() 用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中

throw再度抛出

当一个异常已经被处理了,将不会再次被捕捉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ArrayIndex {
public static void k(){
try{
int[] a = new int[10];
a[10] = 10;
System.out.println("hello");
}catch(ArrayIndexOutOfBoundsException e){//k中已经处理了异常
System.out.println("k error");
}
}

public static void main(String[] args) {
try{
k();
}catch(ArrayIndexOutOfBoundsException e){//不会再次处理
System.out.println("error");
}
System.out.println("main");
}
}

输出
1
2
k error
main

但可以通过throw主动地再次抛出这个异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ArrayIndex {
public static void k(){
try{
int[] a = new int[10];
a[10] = 10;
System.out.println("hello");
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("k error");
throw e;//捕捉到后在此抛出该异常
}
}
public static void main(String[] args) {
try{
k();
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("error");
}
System.out.println("main");
}
}

输出
1
2
3
k error
error
main

为什么要异常机制

我们希望程序能够根据运行过程中可能出现的各种情况进行处理
早期,函数都有特定的返回值,通过函数内很多的if-else来判断返回什么,以返回值来做相应处理

函数内出现很多与功能无关的if-else,会导致函数可读性很差,而且不利于增加新的功能
异常机制将业务逻辑与异常处理在代码上分开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try{
//业务逻辑都放在一起
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
//如果上面的业务逻辑出了问题,就用下面的catch去处理对应的问题
}catch(fileOpenFailed){
doSomething;
}catch(sizeDeterminationFailed){
doSomething;
}catch(memoryAllocationFailed ){
doSomething;
}catch(readFailed ){
doSomething;
}catch(fileCloseFailed ){
doSomething;
}

异常机制最大的好处就是清晰地分开了正常的业务逻辑代码和遇到情况时的处理代码

异常的抛出和声明

在Java中可以自定义异常。
1、所有异常都必须是 Throwable 的子类。
2、如果希望写一个检查性异常类,则需要继承 Exception 类。
3、如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
RuntimeException继承自Exception

声明一个异常类型
1
2
class MyException extends Exception{
}
声明一个可能会抛出异常的方法
1
2
3
public void f() throws MyException{
throw new MyException();//抛出一个MyException异常
}

所有调用这个方法的地方都必须套上try-catch,来处理可能发生的异常

1
2
3
4
5
6
7
public static void main(String[] args) {
try{
f();
}catch(MyException e){//必须catch该方法会抛出的异常类型
System.out.println("error");
}
}

可以声明并不会真的抛出的异常,但调用该方法的地方必须处理全部可能抛出的异常

1
2
3
4
5
6
7
8
9
10
11
12
public void f() throws MyException,YouException{
throw new MyException();//抛出一个MyException异常
}
public static void main(String[] args) {
try{
f();
}catch(MyException e){//必须catch该方法会抛出的异常类型
System.out.println("MyError");
}catch(YouException e){//必须catch该方法会抛出的异常类型
System.out.println("YouError");
}
}

任何继承了Throwable类的对象都可以被throw
Exception类继承了Throwable,我们通常让自定义的异常类从Exception类得到继承

Exception类的两种构造
1
2
3
//我们在自定义异常类时也通常会有这两种构造
throw new Exception();
throw new Exception("HELP");//可以用这个字符串来表达一些东西

catch的匹配机制

抛出子类的异常会被捕捉父类异常的catch给捉到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//声明两个异常类,YouException继承自MyException
class MyException extends Exception{}
class YouException extends MyException{}

public class Test{

public static void f() throws MyException, YouException {
throw new YouException();//抛出一个YouException异常
}

public static void main(String[] args) {
try{
throw new YouException();
}catch(MyException e){//MyException匹配到了它的子类YouException
System.out.println("YouException");
}
}
}

如果同时捕捉父类子类两个异常,子类catch要写在父类前面,否则会报错
1
2
3
4
5
6
7
try{
throw new YouException();
}catch(YouException e){
System.out.println("YouException");
}catch(MyException e){
System.out.println("YouException");
}

捕捉任何异常

1
2
catch(Exception e){
}

运行时刻异常

像ArrayIndexOutOfBoundsException这样java本身提供的异常是不需要声明的,如果需要去声明这些异常,那么每个方法都将带上一长串的声明
但是如果没有适当的机制来捕捉,就会最终导致程序终止

异常遇到继承

当覆盖一个方法的时候,子类不能声明抛出比父类的版本更多的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyException extends Exception{}
class YouException extends MyException{}
class NewException extends Exception{}

public class Test {
public static void f() throws MyException {}
public static void main(String[] args) {}
}
class NewClass extends Test{
//NewClass'中的f()'与' Test'中的'f()'冲突;重写的方法未抛出NewException'
public void f() throws NewException {}
//正确的
public void f() throws YouException {}
public void f() throws MyException {}
public void f() {}
}

在子类的构造方法中,必须声明父类可能抛出的全部异常,可以抛出更多异常,可以是父类抛出异常的父类异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyException extends Exception{}
class YouException extends MyException{}
class NewException extends Exception{}

public class Test {
public Test() throws YouException{}
public static void main(String[] args) {}
}

class NewClass extends Test {
//允许
public NewClass() throws YouException{}
public NewClass() throws MyException{}
public NewClass() throws MyException,NewException{}
//不允许
public NewClass(){}
public NewClass() throws NewException{}
}


Stream流

任何程序都有输入输出,会向用户那读点东西,也会向用户那输出点东西
所以,任何一个编程语言都给程序员提供了输入输出的方式,让这个程序可以和外界打交道

对于java语言,以及之后的新语言,处理输入输出的手段叫做

流是一个抽象、动态的概念,是一连串连续动态的数据集合。
流给数据源和程序之间提供了数据信息传输的通道,编程语言提供了多种流用于数据传输

Hallo World

1
2
3
4
5
public class Main {
public static void main(String[] args) throws IOException {
System.out.println("Hello World!");
}
}

这个程序就用到了输出流,把Hallo World输出给用户看
System是一个类,out是这个类的一个静态成员,println是这个成员能做的事情
实际上,out这个成员就是用于做输出的流

流的基础

在java的基础类库中,所有的输出都基于OutputStream类,所有的输入都基于InputStream类,这两个类构成了输入和输出的基础

但这两个类是抽象的,具体使用时应该用它们的子类
在这里可以看到java系统类库中所有的包Java®平台、标准版和Java开发工具包第18版API规范
在其中的java.base中有java.io,这里面有java输入输出所有相关的东西
在里面可以找到InputStreamOutputStream类,当然这里面还有很多的类,以及其它的东西

点击InputStream,可以看到这个类的描述,以及它所有的方法

InputStream把外界的输入当做字节的流来看待,OutputStream也同理,当我们使用这两个类,只能做字节层面上的读和写

尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;

public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
byte[] buffer = new byte[1024];//一个1k字节的butter
int len = 0;//让系统读取输入流,并将读取到的内容存储到buffer中,返回读取到的字节数
try {//所有io的操作都存在风险,所以要捕获异常
len = System.in.read(buffer);
String s = new String(buffer, 0, len);//将buffer中从0开始到len的这么多个字节构造一个字符串
System.out.println("读到了:"+len+"字节");//输出读取到的字节数
System.out.println(s);//输出字符串
System.out.println("s的长度:"+s.length());//输出字符串有多少个字符
} catch (IOException e) {
e.printStackTrace();
}
}
}

输出
1
2
3
4
5
6
7
Hello World!
输入:123abc
读到了:7字节
123abc

s的长度:7


123abc是6个字节,但后面还有个回车,所以读到7个字节,且输出s时,把回车也输出了

换个输入:

输出
1
2
3
4
5
6
7
Hello World!
输入:123汉字abc
读到了:13字节
123汉字abc

s的长度:9


在UTF-8编码中,一个中文字符等于三个字节,所以一共读到13个字节

文件流

System.inSystem.in是标准输入和标准输出的流
如果想要直接写文件,就需要使用到文件流FileInputStreamFileOutputStream

实际工程中已经较少直接对文件进行读写(除了在造轮子)
更常用的是以在内存数据或通信数据上建立的流,如数据库的二进制数据读写或网络端口通信
具体的文件读写往往有更专业的类,比如配置文件和日志文件

尝试一下FileOutputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.FileOutputStream;
import java.io.IOException;

public class test {
public static void main(String[] args) {
System.out.println("Hello World!");
byte[] buf = new byte[10];//10个字节的数组
for (int i=0; i<buf.length; i++){
buf[i] = (byte)i;//让buf中的每个元素都是i,而且是byte类型
}
try {
FileOutputStream out = new FileOutputStream("a.dat");//如果文件不存在,则创建,存在则覆盖
out.write(buf);//将buf中的数据写入文件
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Hexdump打开这个16进制文件
1
2
3
  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: 00 01 02 03 04 05 06 07 08 09 ..........


1到9都已经被写入

流过滤器

无论是System.in和out还是文件流,都只能处理单个字节,一个个字节地读,一个个字节地写
如果要把一个10进制地整数写入到一个文件中,就要同时读写4个字节,显然前面介绍的流无法做到

流过滤器可以在已存在的流的基础上,去增加一层层的过滤器,每一层的过滤器都可以做点事情,其中一些过滤器就可以做int、double这些基础类型数据的读和写

尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class test {
public static void main(String[] args) {
System.out.println("Hello World!");
byte[] buf = new byte[10];//10个字节的数组
for (int i=0; i<buf.length; i++){
buf[i] = (byte)i;//让buf中的每个元素都是i,而且是byte类型
}
try {
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(//缓冲输出流
new FileOutputStream("a.dat")));
//打开这个文件之后,在上面接了一个缓冲流,缓冲流外面还有一个流,最终得到的是一个DataOutputStream的对象
//我们可以往DataOutputStream这个流里面写入数据,数据会被缓冲到缓冲流里面,缓冲流里面的数据会被写入到文件里面
int i = 0xcafebabe;
out.writeInt(i);//写入一个int类型的数据,DataOutputStream流的writeInt方法可以写入一个int类型的数据
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}


1
2
  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE J~:>

0xcafebabe这个16进制数已经被写入
输出
1
2
Hello World!
-889275714

0xcafebabe这个16进制的数对应的10进制数是-889275714

每一层过滤器都可以起到一定的作用,在上面的程序中,BufferedOutputStream起到了缓冲垫作用,DataOutputStream起到了读写基本数据类型的作用

文本的输入和输出

加上了DataOutputStream也只能以二进制处理基本数据类型,如何处理文本?

二进制数据采用InputStream/OutputStream
文本数据采用Reader/Writer

但Reader/Writer本身是处理Unicode编码的字符的,如果文件是Unicode编码,可以直接用Reader/Writer处理文件,但一般情况下,文件本身并不是Unicode编码,它可能是GBK,可能是UTF-8
在这种情况下,我们需要借助Stream,用字节形式打开文件,再在Stream流的基础上,用过滤器的方式去建立Reader/Writer ,来做文本的输入和输出,StreamReader可以将字节流转换为字符流,然后交给Reader/Writer,当然中间可以加上Buffered实现缓冲

尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.*;

public class test {
public static void main(String[] args) {
System.out.println("Hello World!");
byte[] buf = new byte[10];//10个字节的数组
for (int i=0; i<buf.length; i++){
buf[i] = (byte)i;//让buf中的每个元素都是i,而且是byte类型
}
try {
PrintWriter out = new PrintWriter(//创建一个输出流,PrintWriter可以通过连接BufferedWriter实现的缓冲功能
new BufferedWriter(//创建一个缓冲流
new OutputStreamWriter(//将字节流转换为字符流
new FileOutputStream("a.txt"))));//创建一个文件输出流
//我们做了一个文件流,但它只能处理字节
// 在此基础上再做一个桥梁:OutputStreamWriter
// 它构建起了Stream和Writer的桥梁,它的输入是OutputStream,输出是Writer
int i = 123456;
out.println(i);//在PrintWriter的基础上,我们可以用println方法来输出数据
out.close();
//和上面输出一样,如果要读取一个文件,我们需要做一个桥梁:InputStreamReader
//它构建起了Stream和Reader的桥梁,它的输入是InputStream,输出是Reader
BufferedReader in = new BufferedReader(// BufferedReader类从字符输入流中读取文本并缓冲字符
new InputStreamReader(//将字节流转换为字符流
new FileInputStream("src/test.java")));//创建一个文件输入流,打开这个程序的源码文件
//readLine()方法从字符输入流中读取一行,并返回该行。
String line;
while ((line = in.readLine()) != null){//当读取到的行不为空时,执行循环,就可以读取整个文件
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

运行:在a.tet文件中有123456字符,控制台也将这整个源码输出了出来

除了BufferedReader,还有LineNumberReader,里面的getLineNumber()可以读取指定的行

FileReader是InputStreamReader类的子类,所有方法都从父类中继承而来,它可以直接读取一个二进制文件,建立起一个流,然后形成一个Reader
FileReader(File file)在给定从中读取数据的Fe的情况下创建一个新FileReader
FileReader(String fileName)在给定从中读取数据的文件名的情况下创建一个新FileReader
FileReader不能指定编码转换方式

汉字编码问题

FileOutputStream(“a.txt”)可以以二进制形式打开一个文件
OutputStreamWriter(FileOutputStream(“a.txt”))将字节流转换为字符流,如果不指定编码,那么它优先使用程序源码文件的编码去转换这个字节流为字符流,这就很容易导致汉字乱码

我们可以指定编码进行转换OutputStreamWriter(FileOutputStream(“a.txt”),”utf8”)
用utf8去将这个字节流转换成字符流

当然还有其它方法,这里不展开讲

格式化输入输出

格式化输出:使用printf(“格式”, );用法和C语言的基本一样,这里不展开讲,博主同样有篇C语言学习笔记

格式化输入:如果想从一个文本中读取出一些数字,可以在流上构建一个Scanner,然后用next系列的方法去读取数字、单词等等

Stream/Reader/Scanner的选择

流的应用

现在已经很少有程序需要用流的方式去打开一个文件,裸地去进行文件读和写的操作,更多地是从某个地方得到了一个流

服务器通信

下面这个程序将从本地服务器得到一个流,并向这个流写入东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

public class socket {
public static void main(String[] args) {
try {
Socket socket = new Socket(InetAddress.getByName("localhost"), 12345);//创建一个Socket对象,指定服务器地址和端口号
PrintWriter out = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
//getOutputStream()方法获取Socket对象的输出流,并构造一个BufferedWriter对象
socket.getOutputStream())));//创建一个PrintWriter对象,用于向服务器发送信息
out.println("Hello, world!");//向服务器发送一行文本
out.close();//关闭PrintWriter对象
socket.close();//关闭Socket对象
} catch (IOException e) {
e.printStackTrace();
}
}
}

当然,直接运行这个程序一定报错,因为本地没有任何服务器程序在12345端口上听着,连接建立不起来

使用netcat可以实现监听
netcat下载netcat1.12
解压后把nc.exe移动到C:\Windows\System32目录,压缩包内其它东西用不上
然后打开cmd,运行nc -l -p -12345

现在再运行程序,cmd窗口就会输出Hello, world!

还可以接收服务端的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

public class socket {
public static void main(String[] args) {
try {
Socket socket = new Socket(InetAddress.getByName("localhost"), 12345);//创建一个Socket对象,指定服务器地址和端口号
PrintWriter out = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
//getOutputStream()方法获取Socket对象的输出流,并构造一个BufferedWriter对象
socket.getOutputStream())));//创建一个PrintWriter对象,用于向服务器发送信息
out.println("Hello, world!");//向服务器发送一行文本
out.flush();//刷新缓冲区,将缓冲区中的数据立即发送出去
BufferedReader in = new BufferedReader(
new InputStreamReader(
//getInputStream()方法获取Socket对象的输入流,并构造一个BufferedReader对象
socket.getInputStream()));//创建一个BufferedReader对象,用于接收服务器端的信息
String line = in.readLine();//读取服务器端的一行文本
System.out.println(line);//输出读取的文本
out.close();//关闭PrintWriter对象
socket.close();//关闭Socket对象
} catch (IOException e) {
e.printStackTrace();
}
}
}

在cmd窗口输入任意文本,idea的控制台也会输出这个文本

对象串行化

写入和读取一个对象,使用ObjectOutputStream和ObjectInputStream,被读写的类要实现Serializable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.io.*;

class Student implements Serializable {//可以串行化的类
private String name;
private int age;
private int grade;

public Student(String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}

public String toString() {
return "Student: " + name + " " + age+ " " + grade;
}
}

public class chh {
public static void main(String[] args) {
try {
Student s1 = new Student("zhangsan", 20, 1);
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("obj.dat"));
out.writeObject(s1);
out.close();
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("obj.dat"));
Student s2 = (Student) in.readObject();
System.out.println(s2);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}


obj.bat
1
2
3
4
5
6
7
8
  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: AC ED 00 05 73 72 00 07 53 74 75 64 65 6E 74 98 ,m..sr..Student.
00000010: 61 28 66 C5 BE 55 BC 02 00 03 49 00 03 61 67 65 a(fE>U<...I..age
00000020: 49 00 05 67 72 61 64 65 4C 00 04 6E 61 6D 65 74 I..gradeL..namet
00000030: 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 ..Ljava/lang/Str
00000040: 69 6E 67 3B 78 70 00 00 00 14 00 00 00 01 74 00 ing;xp........t.
00000050: 08 7A 68 61 6E 67 73 61 6E .zhangsan


输出
1
Student: zhangsan 20 1


完结撒花!
javase的学习暂且告一段落,但学习的步伐永不停歇
翁恺老师讲的课循序渐进,简洁明了,好评,但课程上只学到流,刚步入javase的高级部分
后面还有一个重要部分:线程
过段时间实操一个java的管理系统(万物起源管理系统),暂且咕咕咕吧