Java拾遗

前言

Java 语言基础中知识点繁多, 大多数只是在第一次学习时有印象, 但是因为长时间没有运用到会遗忘, 这篇博文就用来记录和总结遇到的生僻的知识点.


抽象类

abstract 修饰符可以用来修饰方法, 也可以用来修饰类, 如果修饰方法, 那么该方法就是抽象方法; 如果修饰类, 那么该类就是抽象类.

  • 不能new 抽象类, 只能靠子类去实现它
  • 抽象类中可以写普通方法
  • 抽象方法必须在抽象类中

接口

  • 在接口中定义的常量会默认加上修饰符 public static final, 在实现了该接口的类中可以直接拿来用
  • 在接口中定义的方法会默认加上修饰符 pubilc abstract
  • 接口中没有构造方法, 这是不能被实例化的原因

内部类

成员内部类

定义以下的类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Outer {
private int id;
public void out(){
System.out.println("这是外部类的方法");
}

class Inner{
public void in(){
System.out.println("这是内部类的方法");
}
}
}

两个类中分别定义了各自的方法. 然后实例化外部类的对象, 对该类进行测试.

1
2
3
4
5
6
7
public class Application {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.in();
}
}

这里就要注意 new 内部类对象时写法上的区别了.

可以通过定义内部类的方法来获取外部类的私有属性, 修改外部类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Outer {
private int id = 10;

public void out() {
System.out.println("这是外部类的方法");
}

class Inner {
public void in() {
System.out.println("这是内部类的方法");

}

public void getId() {
System.out.println(id);
}
}
}

然后通过内部类的对象调用:

1
2
3
4
5
6
7
8
public class Application {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.in();
inner.getId();
}
}

输出为:

静态内部类

在定义内部类时添加 statc 关键字即可.

因为创建内部类的方式与调用方法的步骤与成员内部类相同, 所以就不再赘述.

但是需要注意的是, 静态内部类是随外部类加载时创建的, 所以不能调用成员变量.

局部内部类

在外部类的方法中定义的类叫局部内部类.

1
2
3
4
5
6
7
8
9
10
11
12
public class Outer {
private int id = 10;

public void method(){
class Inner{
public void in(){
System.out.println("这是内部类的方法");
}
}
}

}

异常

Error:

通常是灾难性的致命的错误, 是程序员无法控制和处理的, 当出现这些异常时, JVM一般会终止线程

Exception:

通常情况下是可以被程序处理, 并且在程序中应该尽可能的去处理这些异常

抛出异常

主动抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int a = 1;
int b = 0;

new Demo01().test(a, b);


}
public void test(int a, int b){
if (b==0){
throw new ArithmeticException(); // 主动抛出异常, 一般在方法中使用
}
}

假设方法中处理不了这个异常, 直接在方法上抛出, 使用throws 关键字, 然后调用该方法时用try-catch 捕获处理

捕获异常

使用try-catch捕获异常并处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
int a = 1;
int b = 0;

try {
new Demo01().test(a, b);
} catch (ArithmeticException e) {
System.out.println("运算失败, 除数为0");
} finally {
System.out.println("调用结束");
}


}
public void test(int a, int b) throws ArithmeticException{
System.out.println(a/b);
}

输出为:

1
2
运算失败, 除数为0
调用结束

异常处理五个关键字

try, catch, throw, throws, finally

先看try , catch, finally 关键字的常见用法

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
int a = 1;
int b = 0;

try { // try监控区域
System.out.println(a / b);
} catch (Exception e) { // catch(想要捕获的异常类型)捕获异常
System.out.println("程序出现异常, 变量b不能为0");
} finally { // 处理善后工作
System.out.println("finally");
}
}

输出结果为:

1
2
程序出现异常, 变量b不能为0
finally

这里需要注意的是finally, 不管是否能捕获到异常, 都会执行该代码块, 属于善后工作, 也可以不要

可以写多个catch 进行捕获, 如果要写多个的话,小的异常写上面, 层层递进, 大的写上面就给覆盖掉了

thowthrows 的区别在于, 前者为方法中主动抛出异常, 后者是该方法中处理不了让调用该方法的方法通过try-catch处理.

自定义异常

创建自定义异常要继承Exception

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

// 传递数字 > 10
private int detail;

public MyException(int a) {
this.detail = a;
}

// toString: 异常的打印信息
@Override
public String toString() {
return "MyException{" + detail + '}';
}
}

然后使用该异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyExceptionTest {
// 可能会存在异常的方法
static void test(int a) throws MyException {
System.out.println("传递的参数为: " + a);
if (a>10){
throw new MyException(a);
}
System.out.println("OK");
}

public static void main(String[] args) {
try {
test(11);
} catch (MyException e) {
System.out.println(e);
}
}
}

输出为:

1
2
传递的参数为: 11
MyException{11}

在这段代码中, 判断a>10 的时候抛出我们所定义的异常, 这里有两种处理方式, 一是在该方法中try-catch 进行异常的处理, 或者在方法层面抛出, 让该方法的调用者去try-catch 去处理. 这里采用的是第二种处理方法.

总之, 只要抛出了异常, 要么就地处理, 要么抛出去谁调用谁处理, 总是要还的.

以下是就地处理的代码和结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyExceptionTest {
// 可能会存在异常的方法
static void test(int a) {
System.out.println("传递的参数为: " + a);
if (a>10){
try {
throw new MyException(a);
} catch (MyException e) {
System.out.println(e);
}
}
System.out.println("OK");
}

public static void main(String[] args) {
test(11);
}
}

输出为:

1
2
3
传递的参数为: 11
MyException{11}
OK

要注意输出的结果是不一样的!!


类加载器与构造器的调用顺序

今天遇到一个考察继承的题目, 牵涉到类加载器和构造器调用顺序的问题, 很有趣.

: new一个C02对象, 会输出什么信息?

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class A02 {
private static int n1 = getVal01();
static {
System.out.println("A02的一个静态代码块..");
}
{
System.out.println("A02的第一个普通代码块..");
}
public int n3 = getVal02();
public static int getVal01() {
System.out.println("getVal01");
return 10;
}

public int getVal02() {
System.out.println("getVal02");
return 10;
}

public A02() {
System.out.println("A02的构造器");
}

}

class B02 extends A02 {

private static int n3 = getVal03();

static {
System.out.println("B02的一个静态代码块..");
}
public int n5 = getVal04();
{
System.out.println("B02的第一个普通代码块..");
}

public static int getVal03() {
System.out.println("getVal03");
return 10;
}

public int getVal04() {
System.out.println("getVal04");
return 10;
}
public B02() {
System.out.println("B02的构造器");
// TODO Auto-generated constructor stub
}
}

class C02 extends B02 {

private static int n6 = getVal06();

static {
System.out.println("C02的一个静态代码块..");
}

{
System.out.println("C02的第一个普通代码块..");
}
public int n8 = getVal08();

public static int getVal06() {
System.out.println("getVal06");
return 10;
}

public int getVal08() {
System.out.println("getVal08");
return 10;
}
public C02() {
System.out.println("C02的构造器");
}

public static void main(String[] args) {
C02 c = new C02();

}
}

分析:

这里有三个类,A02是父类, B02继承A02, C02继承B02. 每个类都有各自的静态代码块和无参构造器.

根据继承的特点可知, 当new 一个C02对象时, 会去调用C02的构造器, C02构造器第一行默认为super() , 即调用父类B02的构造器, 同理调用A02 的构造器, 然后顺序执行类A02 的代码 -> A02构造器内的代码 -> 顺序执行类B02 的代码 -> B02构造器内的代码 -> 顺序执行C02 的代码 -> C02 构造器内的代码.

然而加了类构造器之后该是什么顺序呢? 直接贴结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getVal01
A02的一个静态代码块..
getVal03
B02的一个静态代码块..
getVal06
C02的一个静态代码块..
A02的第一个普通代码块..
getVal02
A02的构造器
getVal04
B02的第一个普通代码块..
B02的构造器
C02的第一个普通代码块..
getVal08
C02的构造器

我们刚才分析的步骤是从第七行开始的, 后面全部严格按照分析的步骤打印信息, 所以可以得出结论: 类加载器工作的时间整体都在构造器之前.

不难看出, 类加载器的调用顺序与构造器是类似的, 也是从子类一路到父类, 然后再到子类. 更加严谨的调用顺序如下所述:

类加载器调用顺序

JVM会用类加载器加载xxx.C02这个class文件

加载(class){

  if(class有父类){

    加载(superclass);

  }

  1.静态域申明,默认初始化为0,false,null

  2.按照申明顺序(从上而下书写顺序)执行静态域(赋值)和静态代码块(执行代码块体),

    二者等价,因此不可在静态代码块中使用位于代码块之后申明的静态域,但是可以初始化

  3.按照申明顺序加载静态方法

}

构造器调用顺序

  1. 所有实例域初始化为默认值0,false,null

  2. 按照申明顺序执行域初始化及块初始化

  3. 如果构造器”第一行”调用了其他构造器,则执行

  4. 执行构造器体

方法调用顺序

  1. 编译器查看对象的申明类型,找到它所有与方法名相同的方法

  2. 根据参数类型,找到相应”最合适”的父类方法可能会出现类型转换(向上转型)

  3. 如果是private,static,final,构造器 方法,那么已经确定就是该方法(这四种类型的方法没有多态特征),

  因为没有多态所以也叫静态绑定

  1. 如果是其他方式,采用动态绑定:JVM去寻找改类的实际类型中对应的最合适方法

  2. 执行调用

结合理论表述, 本题的顺序总结来说就是:

  1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
  2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
  3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
  4. 父类的构造方法
  5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
  6. 子类的构造方法
-------------本文结束感谢您的阅读-------------
可以请我喝杯奶茶吗