Java 学习笔记:基础知识梳理

数据

数据类型

基本类型Java 学习笔记:基础知识梳理

  • 为保证 Java 的移植能力,基本类型大小不可改变。
  • 位于栈,直接存储 ”值“,而非引用的自动变量。
  • boolean 只有两个值:truefalse ,可以使用单个字节进行存储,具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int ,使用 1 来表示 true0 来表示 false 。JVM 并不直接支持 boolean 数组,而是使用 byte 数组、 int 数组来表示。

包装类型

基本类型均具有其对应的包装类型,两者之间的赋值使用自动装箱与拆箱完成。

Integer x = 2;	// 装箱
int y = x;		// 拆箱

数据保存

Java

  1. 寄存器 register
    • 位于 CPU 处理器内部,速度最快,其控制权在于编译器,无法人为进行控制。
  2. 堆栈 stack
    • 位于常规 RAM 区域(随机访问存储器),快速、高效,速度仅次于寄存器。灵活性低,数据占用空间已知。
    • 用于存放基本类型变量、句柄以及方法的形参(方法调用完成后即回收)。
  3. 堆 heap
    • 位于常规 RAM 区域(随机访问存储器),可动态分配内存大小,比较灵活,但是存取速度较慢。
    • 用于存放由 new 创建的对象与数组以及 this
  4. 静态存储 static storage
    • 位于常规 RAM 区域(随机访问存储器),位于固定位置。
    • 用于存放程序运行中一直存在的数据以及对象中的特定元素,而不是对象本身。
  5. 常数存储 constant storage
    • 位于代码内部,永不改变。
    • 严格保护的数据可置入只读存储器(ROM)
  6. 非 RAM 存储
    • 存放独立于程序的数据,其不受程序控制。可根据需要恢复为普通的、基于 RAM 的数据。
    • 实例:持久化对象(存放在磁盘中)、流式对象(存放在字节流中)
  • 就速度而言,存在关系:寄存器 > 堆栈 > 堆 > 其他。

JVM 内存分区

  1. 堆区 heap
    • 用于存放对象(不包含基本类型)本身,每个对象都包含一个与之对应的 class 信息(类类型,通过 C.getClass() 等方式获取),class 目的是得到操作指令。
    • JVM 中仅有一个堆区被所有线程共享。
  2. 栈区 stack
    • 每个线程包含一个自身的栈区,用于存放基本数据类型的对象以及自定义对象的引用,访问权限私有,其他栈不可访问。
    • 栈 = 基本类型变量区 + 执行环境上下文 + 操作指令区(存放操作指令)
  3. 方法区 method
    • 也称作 “静态区”,特性与堆相同,被所有线程共享。
    • 用于存放所有的 classstatic 对象。

缓存池

new Integer(123)Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
    • valueOf() 首先判断缓存池中是否存在当前值,若存在则返回缓存池中的内容。
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Java 1.8 中,Integer 缓存池的大小默认为 -128 ~ 127

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

编译器会在自动装箱过程中调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,则会引用相同的对象,导致如下结果:

Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓存池如下:

* boolean values true and false
* all byte values
* short values between -128 and 127
* int values between -128 and 127
* char in the range \u0000 to \u007F

在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。

String 类

概览

String 类是字符串操作中应用最广泛的类,其被声明为 final,不可被继承,对象状态不可改变1,具有只读特性。

此处所说的状态包括:

  1. 基本数据类型的值
  2. 引用类型的变量指向的对象
  3. 引用类型指向的对象的状态

在 Java 8 中, String 内部使用 char 数组存储数据;在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。上述数组均被声明为 final,保证 String 类不可变性。

不可变性优势

  1. 可用于缓存 hash 值

    由于 String 的 hash 值经常被使用,例如 String 用作 HashMap 的 key。不可变的特性使得 hash 值也不可变,则仅需进行一次计算。

  2. 满足字符串常量池要求

    如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的时候才可以实现上述使用。

Java 学习笔记:基础知识梳理

  1. 安全性

    String 作为参数时可保证参数不可变,例如其在作为网络连接参数的情况下,若发生改变则会导致连接中出现问题。

  2. 线程安全

    不可变性具备线程安全,可在多个线程中安全地使用。

String 对象处理

对象连接方式

  1. 调用 append() 方法直接连接

    潜在问题:每次调用都会产生一个新对象,可能需要过多的垃圾回收。

  2. + / += 操作符重载

    适用于简单情况,其包括的隐含实现流程如下:

    1. 编译器自动创建 StringBuilder 对象 s
    2. 编译器调用 s.append() 方法实现连接
    3. 编译器调用 s.toString() 方法生成最终连接后的对象
  3. 显式创建 StirngBuilder

    显式实现方法 2 中内容。其前身为 StringBuffer,线程安全但是开销过大。

String, StringBuffer 以及 StringBuilder

1. 可变性

  • String 不可变
  • StringBufferStringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

模式与匹配

工具包

java.util.regex

流程

  1. String 类型的正则表达式生成 Pattern 对象2

    Pattern p = Pattern.compile(args [1])
    
  1. 将待匹配的字符串传入 Pattern 对象的 matcher() 方法

    Matcher m = p.matcher(args[0])
    
  2. matcher() 方法自动生成 Matcher 对象

  3. 调用 Matcher 对象进行后续操作(如是否匹配、何等条件匹配等)

Matcher 对象可复用,利用 m.reset(String s)可应用于新的字符串序列,无参数时可将其重置到当前字符序列的起始位置。

正则表达式

概念

以某种方式描述字符串,多数接受 CharSequence 类型的参数。

基本标准实例
意义 表示
任意字符 *
反斜线 \\
普通制表、换行 \t、\n
包含特定字符的任何字符 [abc] 或是 a|b|c
除了特定字符之外的任何字符 [^abc]
空白符、非空白符 \s、\S
数字、非数字 \d 或是 [0-9]、\D 或是 [^0-9]
词字符、非词字符 \w 或是 [a-zA-Z0-9]、\W 或是 [^\w]
逻辑操作符
意义 表示
Y 跟在 X 后面 XY
X 或 Y X|Y
捕获组 capturing group (X)
  • 可以在表达式中使用 \i 引用第 i 个捕获组
复杂实例
意义 表示
可能有一个负号,后面跟着一位或多位数字 -?\\d+
起始字符可能是一个 - 或 + 或两者都没有 (-|\\+)?
  • + 有特殊意义所以需要 \\ 转义
量词

定义:描述模式吸收输入文本的方式

分类:

  1. 贪婪型 X?

    发现尽可能多的匹配

  2. 勉强型 X??

    匹配满足模式所需的最少字符数

  3. 占有型 X?+

    不保存匹配的中间状态,防止回溯。

字符串常量池 String Pool

概念及原理

字符串常量池(String Pool)保存着所有的字符串字面量(literal strings),这些字面量在编译时期就已经确定。

不仅如此,还可以使用 Stringintern() 方法在运行过程中将字符串添加到字符串常量池中。

当一个字符串调用 intern () 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals () 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

若直接使用字面量的形式创建字符串,则会自动放入字符串常量池中。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4);           // true
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6);			// true
String s7 = "aaa";
System.out.println(s1 == s7); 			// false
String s8 = "hello";
String s9 = "he"+"llo";
System.out.println(s8 == s9); 			// true

在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,由于永久代空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误,字符串常量池被移到堆中。

new String(“abc”)

若当前字符串常量池中不存在 "abc" 字符串对象,使用这种方式一共会创建两个字符串对象:

  1. "abc" 属于字符串字面量,因此编译时期会在 字符串常量池 中创建一个字符串对象,指向这个 "abc" 字符串字面量
  2. 使用 new 关键字会在中创建一个字符串对象,使用 "abc" 作为构造参数

程序流程控制

运算符

优先级

Ulcer Addicts Really Like C A lot
Java 学习笔记:基础知识梳理

参数传递

Java 的参数传递是以 值传递 的方式实现的,而不是引用传递。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。

  • 如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
  • 如果在方法中使指针引用其他对象,那么此时这两个指针指向的是完全不同的对象,在一方改变其所指向对象内容时对另一方没有影响。

类型转换

显式类型转换

Java 不能隐式执行向下转型,因为这会使得精度降低。若需要时则使用强转操作,显式向下转型。

  • 1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。1.1f 字面量才是 float 类型。
// float f = 1.1;
float f = (float) 1.1;
float f = 1.1f;

隐式类型转换

因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。但是使用 += 或者 ++ 运算符可以执行隐式类型转换。相当于将 s1 + 1 的计算结果进行了向下转型。

short s1 = 1;
// s1 = s1 + 1;
s1 += 1;
s1++;
s1 = (short) (s1 + 1);
特殊情况:移位符与等号组合使用于 byte、short 小类型时:
  1. 自动转换为 int 类型
  2. 移位
  3. 截断后赋值给原来类型
  4. 得到错误结果

执行控制

for 循环子语句执行次序

for(A; B; C) {
    D;
}

上述代码块执行次序为:ABDCBDCBD……(直到B条件无法满足)

中断与继续

return

退出当前方法并返回值(如果有)

break

退出循环,不执行剩下语句,结束循环

continue

结束本次循环,退回循环起始再次开始新的循环

  • 上述两种中断均仅退出最内层循环

标签 label

格式:label1:

位置:位于循环语句之前

作用:中断语句中断到存在标签的地方

continue label1;	// 从标签处继续循环
break label1;		// 中断所有循环并不再进入

switch

Java 7 之前仅能使用 int / char 等小类型的值作为选择因子;从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。

截止目前可用选择因子:char、byte、short、int、Character、Byte、Short、Integer、String 或枚举对象 enum

String s = "a";
switch (s) {
    case "a":
        System.out.println("aaa");
        break;
    case "b":
        System.out.println("bbb");
        break;
}

初始化与清除

构造器

设计规则

  1. 用尽可能简单的方法使对象进入就绪状态
  2. 如果可能,避免调用任何方法。可调用 final 属性方法

调用顺序

  1. 父类构造器

    保证子类能够访问的所有成员有效

  2. 按照声明顺序调用成员初始化模块

    保证构造器内部成员有效

  3. 子类构造器

默认情况下子类构造器内会进行父类构造器的自动调用;若含有自变量则需要明确编写调用代码,利用 super() 实现。

初始化

顺序

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

方法重载

唯一区分方式

参数类型列表

主类型重载注意类型转换

  • 若传入类型小于声明类型则被提升
  • 若传入类型大于声明类型则需要进行窄化转换
  • char 类型若找不到对应的参数列表,则自动提升为 int

清除

垃圾回收器

  • 不等于析构函数
  • 仅释放由 new 分配的内存,无法释放对象的特殊内存
  • 对象可能不会被回收
  • 仅在 JVM 面临内存耗尽时进行垃圾回收,不到必要时不回收

finalize()

无法直接调用,仅在特殊场景使用,如 native 方法等非 java 方法。

复用类

访问权限

包:库单元

分割单个全局命名空间

import

导入其他编译单元

package

指出当前编译单元,该语句必须作为文件的第一个非注释语句出现

限制

  • 每个编译单元仅有 0 个或一个 public 类,名称与编译单元文件名相同
  • 每个编译单元具有任意数量非 public 类,用于为 public 类提供支持

访问指示符

  • public
    • 公开
  • private
    • 本类可访问
    • 用于构造器时可形成单例模式,有且只有一个实例
  • protected
    • 同一个编译单元内可以*访问
    • 子类可访问
  • friendly / 包访问权限
    • 无指定时默认为该权限
    • 仅当前包成员可访问

类访问权限仅可是 public 或 包访问权限(内部类除外)。

设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。

如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。

组合

实现

在新类内置入对象句柄,在多数情况下访问权限设置为 private。为了节省开销,句柄不会被默认初始化。

类间关系:包含。

适用场景

  1. 利用现有类特性,而无需使用接口
  2. 现有类型再生、重复使用
  3. 更加灵活,可以动态选择类型或行为

继承

类间关系:

  1. 属于

    纯继承或使用时纯替换

  2. 类似于

    子类属于父类的一种类型

继承过程中禁止消除访问权限

super

  • 访问父类的构造函数:可以使用 super () 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
  • 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。

方法修改

重载

存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。

应该注意的是,返回值不同,其它都相同不算是重载。

子类中重载的方法不会隐藏父类版本,方法在不同的地方有多种含义。

覆盖

存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。为了满足里式替换原则,覆盖有有以下两个限制:

  • 子类方法的访问权限必须大于等于父类方法;
  • 子类方法的返回类型必须是父类方法返回类型或为其子类型。

使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。

class A {
    public String show(D obj) {
        return ("A and D");
    }

    public String show(A obj) {
        return ("A and A");
    }
}

class B extends A {
    public String show(B obj) {
        return ("B and B");
    }

    public String show(A obj) {
        return ("B and A");
    }
}

class C extends B {
}

class D extends B {
}

public class Test {

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        C c = new C();
        D d = new D();
        System.out.println(a1.show(b)); // A and A
        System.out.println(a1.show(c)); // A and A
        System.out.println(a1.show(d)); // A and D
        System.out.println(a2.show(b)); // B and A
        System.out.println(a2.show(c)); // B and A
        System.out.println(a2.show(d)); // A and D
        System.out.println(b.show(b));  // B and B
        System.out.println(b.show(c));  // B and B
        System.out.println(b.show(d));  // A and D
    }
}

涉及到覆盖时,方法调用的优先级为:

  • this.show(O)
  • super.show(O)
  • this.show((super)O)
  • super.show((super)O)

适用场景

  1. 定制现有类的特殊版本
  2. 接口再生
  3. 累积开发
  4. 需要向上转型的情况
  5. 用继承表达行为间的差异并用成员变量表达状态的变化

多态性

可从另一个角度将接口从具体的实施细节中分离出来,消除类间耦合关系,改善代码组织结构与可读性,易于扩展。

向上转型 upcasting

方向

从子类到父类,丢失具体类型信息(子类),仅可调用父类方法。

定义:取得一个对象句柄并将其作为父类 / 接口句柄适用的行为

Shape s = new Circle();

绑定

将方法调用与方法主体连接到一起。

早期绑定

在程序运行以前由编译器与链接程序执行( C 编译器默认)。

后期绑定 / 动态绑定

在运行期间进行,以对象的类型为基础( Java )。

安全

安全性高

向下转型 Downcasting

方向

从父类到子类,获得具体类型信息

安全

安全性需要确认:

  • 类转型异常 ClassCastException
  • RTTI 运行期类型标识,即在运行期间对类型进行检查的行为

抽象类与接口

抽象类

抽象类和抽象方法都使用 abstract 关键字进行声明。

抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。

  • 抽象方法仅含有声明,是不完整的方法
  • 子类需要定义所有抽象方法

抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。

public abstract class AbstractClassExample {

    protected int x;
    private int y;

    public abstract void func1();

    public void func2() {
        System.out.println("func2");
    }
}

public class AbstractExtendClassExample extends AbstractClassExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}

// AbstractClassExample ac1 = new AbstractClassExample(); 
// 'AbstractClassExample' is abstract; cannot be instantiated
AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();

接口

概念及限制

接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。

接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。

接口的字段默认都是 static 和 final 的。

嵌套接口

  • 实现接口时不强制实现内部嵌套接口
  • 嵌套于其中的接口访问权限默认为 public

private 接口

  • 仅被自身使用,不允许在定义其的类之外实现
  • 可强制该接口中的方法不允许向上转型

比较、使用选择及适用场景

比较

  • 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

使用选择及适用场景

使用抽象类
  • 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo () 方法;
  • 需要使用多重继承。

只有在必须使用方法定义或成员变量时才考虑使用抽象类,否则使用接口。

使用接口
  • 需要在几个相关的类*享代码。
  • 需要能控制继承来的成员的访问权限,而不是都为 public。
  • 需要继承非静态和非常量字段。

若确定某事物应成为一个父类,则第一选择应使其成为一个接口。

适用于:

  1. 向上转型到多个父类 / 接口
  2. 创建枚举常量组
  3. 防止生成对象

内部类

定义

将类定义置入一个用于封装它的类内部。

创建

类别

1. 成员内部类(非静态内部类)
public class Out {
	private static int a;
	private int b;
	public class Inner {
		public void print() {
			System.out.println(a);
			System.out.println(b);
		}
	}
}

Out out = new Out();
Out.Inner in = out.new Inner();

2. 嵌套类(静态内部类)
public class Out { 
    private static int a; 
    private int b; 
    public static class Inner { 
        public void print() { 
            System.out.println(a); 
        } 
    } 
}

Out.In obj = new Out.In();

实质上不属于内部类,与外部类没有任何关系。

  • 创建时不需要外部对象
  • 无法访问非静态外部对象,可访问外部类私有静态对象
  • 可置于接口中,可实现外部接口,创建公共代码时使用
  • 可用于放置测试代码,即 main 模块
  • 实例:HashMap 的静态内部类 Entry,是 HashMap 存放元素的抽象。HashMap 内部维护 Entry 数组用了存放元素,但是 Entry 对使用者是透明的。像这种和外部类关系密切的,且不依赖外部类实例的,都可以使用静态内部类。
3. 匿名内部类

没有构造器,初始化可在定义字段时实现(直接使用的外部对象需为 final 属性,若传递给父类构造器则不需要 final)或通过实例初始化方法实现(不可重载)。

  • 限制:不可同时实现接口与继承类;单次仅可实现一个接口。
// 创建一个继承自 C 的匿名类的对象
// 通过 new 表达式返回的引用被自动向上转型为对 C 的引用
public C c() {
    return new C() { 
    //....方法定义
    };
}

4. 局部内部类

即定义在方法中的类,仅在方法中使用。

public class Out { 
    private static int a; 
    private int b; 
    public void test(final int c) { 
        final int d = 1; 
        class Inner { 
            public void print() { 
                System.out.println(c); 
            } 
        } 
    } 
}

位置

  1. 类作用域

    通过生成对外部类对象的引用 Out.this 可任意访问外部类的所有成员

  2. 方法作用域

    在方法之外无法访问,方法结束后对象仍有效

  3. 任意作用域

    类随同其他代码一起得到编译

功能

  • 分组逻辑上相互联系的类
  • 控制类在其他类中的可见性
  • 向上转型
    • 可完全进入不可见/不可用状态:内部类属性设置为 private/protected
    • 向上转型为接口时隐藏实现细节:从用于实现的对象生成接口句柄与向上转型到基础类相同效果
  • 有效实现多重继承
    • 允许继承多个非接口类型(类 / 抽象类)
  • 提供闭包与回调功能
    • 闭包:记录创建其的作用域中信息的对象
      回调:可在运行时动态地决定需要调用的方法

特性

  • 多实例且实例与外部类对象相互独立
  • 多个内部类可以不同方式实现同一个接口/继承同一个类
  • 创建内部类对象的【时刻】与外部类对象的创建无关
    创建一个外部类的时候不一定要创建这个内部类
  • 内部类是一个独立的实体

继承

  • 需传递外部类对象至子类构造器
  • 并在子类构造器中调用外部类对象的 super() 方法
  • 外部类继承
    • 无明确继承内部类
      内部类方法不会被覆盖
      同名内部类属于两个独立空间
    • 明确继承内部类
      内部类方法被覆盖
class WithInner {
  class Inner {}
}

public class InheritInner extends WithInner.Inner {
  //! InheritInner() {} // Won't compile
  InheritInner(WithInner wi) {
    wi.super();
  }
  public static void main(String[] args) {
    WithInner wi = new WithInner();
    InheritInner ii = new InheritInner(wi);
  }
}

选择原则

  • 优先选择类而非接口
  • 从类开始,若接口的必需性变得非常明确,则进行重构
  • 避免接口滥用

Object 通用方法

概览

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native Class<?> getClass()

protected void finalize() throws Throwable {}

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException


equals()

等价关系

// 自反性
x.equals(x); 							// true

// 对称性	
x.equals(y) == y.equals(x); 			// true

// 传递性
if (x.equals(y) && y.equals(z))
    x.equals(z); // true;
    
// 一致性:多次调用方法结果不变
x.equals(y) == x.equals(y); 			// true

// 与 null 比较,对于任何对象均为 false
x.equals(null); 						// false;

等价与相等

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals () 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals () 判断引用的对象是否等价。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); 	// true
System.out.println(x == y);      	// false

实现

public class EqualExample {

    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public boolean equals(Object o) {
        // 检查是否为同一个对象的引用,如果是直接返回 true
        if (this == o) 
            return true;
        
        // 检查是否是同一个类型,如果不是,直接返回 false
        if (o == null || getClass() != o.getClass()) 
            return false;

        // 将 Object 对象进行转型
        EqualExample that = (EqualExample) o;

        // 判断每个关键域是否相等
        if (x != that.x) 
            return false;
        if (y != that.y) 
            return false;
        return z == that.z;
    }
}

hashCode()

equals() 用于判断两个对象是否等价, 而 hashCode() 返回散列值。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); 				// true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
// 由于 EE 类没有实现 hashCode(), 因此两个对象散列值不同
// 导致集合添加了两个等价的对象
System.out.println(set.size());  				// 2

理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。

一个数与 31 相乘可以转换成移位和减法:31*x == (x<<5)-x,编译器会自动进行这个优化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

toString()

默认返回 [email protected] 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

public class ToStringExample {
    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}

ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());

// 输出:[email protected]

clone()

cloneable

clone () 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone (),其它类就不能直接去调用该类实例的 clone () 方法。

public class CloneExample {
    private int a;
    private int b;
}

CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); 
// 'clone()' has protected access in 'java.lang.Object'

重写 clone () 得到以下实现:

public class CloneExample /* implements Cloneable */ {
    private int a;
    private int b;

    @Override
    public CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}

CloneExample e1 = new CloneExample();
try {
    CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}

// 输出:java.lang.CloneNotSupportedException: CloneExample
// 原因:CloneExample 需要实现 Cloneable 接口

  • 应该注意的是,clone () 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone () 方法,就会抛出 CloneNotSupportedException。

浅拷贝

创建一个新对象,然后将当前对象的非静态字段复制到该新对象,**如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。**因此,原始对象及其副本引用同一个对象。

public class ShallowCloneExample implements Cloneable {
    private int[] arr;

    public ShallowCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException {
        return (ShallowCloneExample) super.clone();
    }
}

ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222

深拷贝

深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象。

public class DeepCloneExample implements Cloneable {

    private int[] arr;

    public DeepCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected DeepCloneExample clone() throws CloneNotSupportedException {
        DeepCloneExample result = (DeepCloneExample) super.clone();
        result.arr = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            result.arr[i] = arr[i];
        }
        return result;
    }
}

DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2


clone() 的替代方案

使用 clone () 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone (),可以使用拷贝构造函数或者拷贝工厂来深拷贝一个对象。

public class CloneConstructorExample {

    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}

CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2


关键字

final

final 关键字根据应用场景可分为三个方面:

数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能改变的常量。

  • 对于基本类型,final 使数值不变
  • 对于引用类型,final 使引用不变,即不能引用其他对象,但是被引用的对象本身是可以修改的
final int x = 1;
// x = 2;  // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;

  • 空白 final:定义时无需初始化,实际使用前需要得到正确初始化,灵活性强
  • 参数 / 自变量 final:在方法内部无法改变自变量句柄指向,主要用于向匿名内部类传递数组

方法

声明方法不能被子类重写。

private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

声明类不允许被继承,类中所有方法与数据成员也同时默认为final。

static

静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
public class A {

    private int x;         // 实例变量
    private static int y;  // 静态变量

    public static void main(String[] args) {
        // int x = A.x;  
        // Non-static field 'x' cannot be referenced from a static context
        A a = new A();
        int x = a.x;
        int y = A.y;
    }
}

静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

public abstract class A {
    public static void func1(){
    }
    // public abstract static void func2();  
    // Illegal combination of modifiers: 'abstract' and 'static'
}

只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。

public class A {

    private static int x;
    private int y;

    public static void func1(){
        int a = x;
        // int b = y;  
        // Non-static field 'y' cannot be referenced from a static context
        // int b = this.y;     
        // 'A.this' cannot be referenced from a static context
    }
}

静态语句块

静态语句块在类初始化时运行一次。

public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
// 输出:123

静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*

反射

动态语言

概念

动态语言是指程序在运行时可以改变其结构:新的函数可以引进、已有的函数可以被删除等结构上的变化。

实例

JavaScript、Ruby、Python

从反射角度而言,Java 属于半动态语言。

反射机制概念

每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件保存着 Class 对象。

反射机制:在运行状态中,对于任意一个类都能够知道这个类的所有的属性和方法;并且对于任意一个对象,都能调用其任意一个方法。(动态获取信息、动态调用对象方法)

核心:JVM 在运行时才动态加载类或调用方法 / 访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

反射 API

Class 和 java.lang.reflect 一起对反射提供了支持,

  • Class:反射的核心类,可以获取类的属性、方法等信息

java.lang.reflect 类库主要包含了以下三个类:

  • Field:表示类的成员变量,可使用 get() 和 set() 读取和修改 Field 对象关联的字段;
  • Method:表示类的方法,可使用 invoke() 方法调用与 Method 对象关联的方法;
  • Constructor:表示类的构造方法,可用 Constructor 创建新的对象。

基本运用

获取 Class 对象

类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。

  1. 调用某个对象的 getClass()
Person p = new Person();
Class clazz = p.getClass();

  1. 调用某个类的 class 属性

    不会自动初始化,但是在编译器时进行检查,不会出现运行期异常,安全、高效。

Class clazz = Person.class;

  1. 使用 Class 类中的 forName() 静态方法 (首选)

    会自动初始化,可能抛出 ClassNotFoundException 异常。

Class clazz = Class.forName("Package.ClassName"); // 全路径

当获得了想要操作的类的 Class 对象后,可以通过 Class 类中的方法获取并查看该类中的方法和属性。

// 获取 Class 对象
Class clazz = Class.forName("com.test.Person");

// 获取类的所有方法信息
Method[] method = clazz.getDeclaredMethods();
for(Method m : method) {
    System.out.println(m.toString());
}

// 获取类的所有成员属性信息
Field[] field = clazz.getDeclaredFields();
for(Field f : field) {
    System.out.println(f.toString());
}

// 获取类的所有构造方法信息
Constructor[] constructor = clazz.getDeclaredConstructors();
for(Constructor c : constructor) {
    System.out.println(c.toString());
}

判断是否为某个类的实例

一般地,我们用 instanceof 关键字来判断是否为某个类或其子类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:

public native boolean isInstance(Object obj);

利用 Class 创建实例

  1. 使用 Class 对象的 newInstance()
    • 要求该 Class 对象对应的类有默认的空构造器
Class clazz = Class.forName("Package.Person"); 
Person p = (Person)clazz.newInstance();

  1. 使用 Construct 对象的 newInstance()
Class clazz = Class.forName("Package.Person"); 
Constructor c = clazz.getDeclaredConstructor(String.class, String.class, int.class)
Person p = (Person)c.newInstance("Name", "Male", 20);

适用场景

编译时类型与运行时类型

Person p = new Student();
// 编译时类型为 Person ,运行时类型为 Student。

  • 反射机制适用于【程序需要在运行时发现对象和类的真实信息】的情况。

基于构件的编程

  • 构件需要实例化
  • 构件部分信息需要暴露
  • 相关方法信息可能需要暴露

远程方法调用

  • 允许 Java 程序将对象分布到多台机器上

优点

  • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
  • 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
  • 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

缺点

尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在使用反射技术时,下面几条内容应该牢记于心。

  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
  • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

泛型

泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,即所操作的数据类型被指定为一个参数。

  • 注意:1)基本类型无法作为类型参数;2)静态方法无法访问泛型类的类型参数

    ​ 3)同一个泛型接口的两种变体由于类型参数会被视作相同接口

泛型类

泛型类的声明与非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。泛型类的类型参数声明部分包含一个或多个类型参数,参数间使用逗号分隔。

public class Box<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();

泛型方法

在调用时可以接受不同类型的参数,根据传递给泛型方法的参数类型,编译器适当地处理每个方法调用。

  • 原则:在可以仅泛型化方法时不选择泛型整个类
public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}
public class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

// 实例
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");

// 调用
boolean same = Util.<Integer, String>compare(p1, p2);
// Java 1.7/1.8 利用 type inference 可以自动推导相应类型参数
// 自动推导仅作用于赋值操作,其他操作需要显式指明类型
boolean same1 = Util.compare(p1, p2);

边界符与通配符

类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List<String>,List<Integer> 等所有 List< 具体类型实参 > 的父类。

  1. <? extends T> 设定了边界,表示该通配符所代表的类型是 T 类型的子类
  2. <? super T> 设定了边界,表示该通配符所代表的类型是 T 类型的父类

PECS 原则

由于泛型没有内建的协变类型,即没有内建的编译期与运行期检查,不会进行自动向上转型等操作。若需要建立某种类型的向上转型关系可使用通配符:

List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Orange());
flist.add(null); // Legal but uninteresting

此时 flist 的类型是 List<? extends Fruit>,通配符引用的是明确的类型,即此时类型意味着:【某种 flist 引用没有指定的具体类型】,因此这个被赋值的 List 必须持有诸如 Fruit 或 Apple 这样的某种指定类型,但是为了向上转型为 flist,这个具体类型是什么不做关心。

实现了 <? extends T> 的集合类只能将它视为 Producer 向外提供 (get) 元素,而不能作为 Consumer 来对外获取 (add) 元素。

  • “Producer Extends” – 如果需要一个只读 List,用它来 produce T,那么使用 <? extends T> 。
  • “Consumer Super” – 如果需要一个只写 List,用它来 consume T,那么使用 <? super T> 。
  • 如果需要同时读取以及写入,那么就不能使用通配符了。
public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++)
            dest.set(i, src.get(i));
    }
}

  • 类型未知时可用 contains()、indexOf() 进行读取操作,这两种方法参数均为 Object

类型擦除

原理

Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。

  • 在泛型代码内部,无法获得任何有关泛型参数类型的信息

    如在代码中定义的 List<Object> 和 List<String> 等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。

  • 在使用泛型时,任何具体的类型信息都被擦除

    泛型类型参数将擦除到它的第一个边界:List<T> -> List、T -> Object。

问题及补偿方案

泛型不能用于显式地引用运行时类型的操作之中,即存在以下四个问题:

  • 不允许创建泛型数组

    解决方案:

    1. 创建被擦除类型的新数组
    2. 在集合内部使用 Object[]
    3. 使用前对其进行转型

    上述方案中数据运行期时仍为 Object 等上一级类型

  • 子类在继承(或实现)父类(或接口)的泛型方法时,若在子类中明确指定了泛型类型,则在编译时编译器会自动生成桥接方法(Bridge Method),出现类型转换问题

// 原始类
public class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}
public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

// 类型擦除后
public class MyNode extends Node {
    // Bridge method generated by the compiler
    public void setData(Object data) {
        setData((Integer) data);
    }
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
    // ...
}

// 导致类型转换错误结果
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
// Integer x = mn.data;

  • 只能提供静态类型检查,则类型信息擦除后无法使用类型参数创建实例
public static <E>  void append(List<E> list) {
	E elem = new E();   // compile-time error
    list.add(elem);
}

// 利用反射可解决该问题,调用时加入类型信息传递
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

  • 无法使用 instanceof 关键字
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}
=> { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }

// 可使用通配符重新设置边界
public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

常见面试题

异常

概念

若某个方法无法按照正常的途径完成任务,则可以通过另一种途径退出方法。在这种情况下,会抛出一个封装了错误信息的对象。此时,该方法会立刻退出且不返回任何值。另外,调用该方法的其他代码也无法继续执行,异常处理机制会将代码执行移交给异常处理器。

异常情形(exception condition):指阻止当前方法或作用域继续执行的问题。

分类

Java 学习笔记:基础知识梳理

Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: ErrorException

  • Error

    指 Java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。JVM 无法处理。

  • Exception

    • RuntimeException:运行时异常,非受检。如除 0 操作、空指针等,此时程序崩溃且无法恢复。程序员的错误。
    • CheckedException:受检异常,如 I/O 导致的 IOException、SQLException。一般是外部错误,发生在编译阶段。需要用 try…catch… 语句捕获并处理,可恢复。

处理方式

Java 的异常处理本质上是抛出异常和捕获异常。

抛出异常

对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,需要从当前环境中跳出,并把问题提交给上一级环境,抛出异常后,处理流程如下:

  1. 使用 new 在堆上创建异常对象

    除构造器内部掷出的异常之外,其他异常会被垃圾收集器自动清除。构造器内部掷出的异常需要使用嵌套的 try 语句解决:在创建需要清除的对象后立即进入一个 try-finally 语句块。

  2. 停止当前执行路径(已无法继续)

  3. 从当前环境释放出异常对象的句柄

  4. 异常控制机制接管

    • 简单继续
    • 尝试其他路径

捕获异常

在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。

运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着 Java 程序的终止。

基本语法

public class TestException {  
    public static void main(String[] args) {  
        int a = 1;  
        int b = 0; 
        // try 监控区域 : 一段可能产生异常的代码
        try { 				   
        	// 通过 throw 语句抛出异常  
            if (b == 0) throw new ArithmeticException(); 
            System.out.println("a / b 的值是:" + a / b);  
            System.out.println("this will not be printed!");
        }  
        // catch 捕获异常 
        // 实际上该异常是运行期异常,不必捕获,仅作示例
        catch (ArithmeticException e) {  
            System.out.println(e + ": 程序出现异常,变量 b 不能为 0 !");  
        }  finally {
            
        }
        System.out.println("程序正常结束。");  
    }  
}  

// 输出:
/* D:\java>java TestException 
   java.lang.ArithmeticException: 程序出现异常,变量 b 不能为 0 !
   程序正常结束。
   */
// Throwable 重载了 toString() 方法,所以它将返回一个包含异常描述的字符串

catch 语句捕获匹配的原则

  • 如果抛出的异常对象属于 catch 子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与 catch 块捕获的异常类型相匹配。
  • 按照书写顺序进行顺序匹配,当一个 catch 子句执行后,其他子句被旁路。因此应将最大范围的父类 Exception 置于最后。

throw 与 throws 的区别

位置不同

  • throws 用在函数上,后面可跟一个或多个异常类
  • throw 用在函数内,后面跟随异常对象

功能不同

  • throws 用于声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式。并不一定会发生这些异常。
  • throw 抛出具体问题对象,执行到 throw 时功能就已经结束了,跳转到调用者并将具体问题对象抛给调用者。即 throw 语句独立存在时,后面不定义其他语句,无法到达。执行throw则一定抛出了某种异常对象。
  • 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。

finally

当异常发生时,通常方法的执行将做一个陡峭的非线性的转向,它甚至会过早的导致方法返回。例如,如果一个方法打开了一个文件并关闭,继而退出,但是不希望关闭文件的代码被异常处理机制旁路。finally 关键字为处理这种意外而设计。

  • finally 子句是可选项,可以有也可以无,但是每个 try 语句至少需要一个 catch 或者 finally 子句。
  • finally 子句一定会在 try 语句结束之前执行,若 try 语句与 finally 语句内部都有返回语句 return,则执行 finally 语句中的 return 语句,使得 try 语句中的 return 失效。

异常链

异常链顾名思义就是将异常发生的原因一个传一个串起来,把底层的异常信息传给上层,逐层抛出。即抛出不同类型异常后希望保留原始异常信息,若新抛出的异常类型与原始异常对象相同则会自动保留,无需异常链。 Java API 文档中给出了一个简单的模型:

try {   
    lowLevelOp();   
} catch (LowLevelException le) {   
    throw (HighLevelException) new HighLevelException().initCause(le);   
}

当程序捕获到了一个底层异常,在处理部分选择了继续抛出一个更高级别的新异常给此方法的调用者。 这样异常的原因就会逐层传递。这样,位于高层的异常递归调用 getCause () 方法,就可以遍历各层的异常原因。 这就是 Java 异常链的原理。异常链的实际应用很少,发生异常时候逐层上抛不是个好注意,上层拿到这些异常也无法处理,而且异常逐层上抛会消耗大量资源, 因为要保存一个完整的异常链信息。

自定义异常

使用 Java 内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。用户自定义异常类,只需继承现有相似的 Exception 类即可。

在程序中使用自定义异常类,大体可分为以下几个步骤:

  • 创建自定义异常类。
  • 在方法中通过 throw 关键字抛出异常对象。
  • 异常处理
    • 如果在当前抛出异常的方法中处理异常,可以使用 try-catch 语句捕获并处理;
    • 否则在方法的声明处通过 throws 关键字指明要抛出给方法调用者的异常,继续进行下一步操作。
  • 在出现异常方法的调用者中捕获并处理异常。
class MyException1 extends Exception {
    String msg;
    MyException1(String s){
        msg = s;
    }

    public void printString() {
        System.out.println(msg);
    }    
}

public class MyException {
    public void f() throws MyException1 {     
            throw new MyException1("throw my exception1");       
    }
    public static void main(String[] args) {
        MyException me = new MyException();
        try {            
            l:
            while (true) {
                System.out.println("inside while");
                me.f();
                break l;
            }
        } catch (MyException1 e) {
            e.printStackTrace();
        } finally {
            System.out.print("finally executed");
        }        
    }
}

// 输出
/*
inside while
MyException1
	at MyException.f(MyException.java:17)
	at MyException.main(MyException.java:27)
finally executed
*/

常见面试题

注解

概念

注解(Annotation)提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素进行关联。其像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中,为程序的元素加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。

Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。

属于语言级内容,一旦构造则享有编译期的类型保护检查,在实际源代码级别保存所有信息。可用以简化与自动化创建描述符性质类或接口的重复性工作。

定义

类似于接口的定义,会被编译为 class 文件

public @interface UseCase{}

特性

  • 可嵌套
  • 可对一个目标同时使用多个注解,此时同一个注解不能重复使用
  • 不支持继承

注解元素

定义

类似于接口方法的定义,区别在于可为其指定默认值。无元素的注解即标记注解。

public int ele() default "123";

可用类型

所有基本类型、String、Class、enum、Annotation 以及上述所有类型构成的数组

默认值限制

元素值要么在具有默认值,要么需要在使用时提供准确值。非基本类型元素不可使用 null 为其值,可以使用空字符串代替。

注解处理器

自定义

Method m = Utils.class.getDeclaredMethods(); // 假定 Utils 中使用了注解 UseCase
UseCase uc = m.getAnnotation(UseCase.class); // 返回指定类型的注解对象
//上述两个反射方法均属于 AnnotatedElement 接口
uc.id() // 获取具体值进行后续处理

工具 apt

概念

javac 内置的一个用于编译时扫描和处理注解的工具,在源代码编译阶段可通过其获取源文件内注解的相关内容。

功能

在编译期间获取相关注解数据继而动态生成 .java 源文件,通常时自动产生一些有规律性的重复代码。

实现

  • AnnotationProcessor 接口:用以抽取注解

    构造器参数:AnnotationProcessorEnvironment,利用其可获取 Messager (向用户报告信息)、Filer(创建新文件)

  • AnnotationProcessorFactory 接口:用以处理注解

    可用方法:

    • getProcessorFor(),参数 AnnotationProcessorEnvironment,返回注解处理器
    • supportedAnnotationTypes()
    • supportedOptions()

应用

访问者模式

适用场景

  • 生成文档。

    这是最常见的,也是 Java 最早提供的注解。常用的有 @param @return 等

  • 跟踪代码依赖性,实现替代配置文件功能。

    如 Dagger 2 依赖注入,未来 Java 开发,将大量注解配置,具有很大用处

  • 在编译时进行格式检查。

    如 @Override 放在方法前,若该方法并没有覆盖超类方法,则编译时就能检查出

原理

注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。而通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象 $Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。

Java SE 5 内置注解

标准注解

  • @Override:标记类型注解,表示当前方法定义将覆盖超类中的方法
  • @Deprecated:标记类型注解,不鼓励使用该注解修饰的程序元素。若使用了该注解修饰的元素则编译器会发出警告
  • @SuppressWarnings:不是标记类型注解,其有一个类型为 String [] 的成员元素,这个成员的值为被禁止的警告名。对于 javac 编译器来讲,被 - Xlint 选项有效的警告名也同样对 @SuppressWarnings 有效,同时编译器忽略掉无法识别的警告名。

元注解

负责注解其他注解

  • @Target:表示该注解可以用在什么地方,默认值为任何元素

    取值 ElementType:

    • CONSTRUCTOR:用于描述构造器
    • FIELD:成员变量、对象、属性(包括 enum 实例)
    • LOCAL_VARIABLE:局部变量(循环变量、catch 参数等)
    • METHOD:描述方法
    • PACKAGE:描述包
    • PARAMETER:描述参数
    • TYPE:描述类、接口、枚举、Annotation 类型或 enum 声明
  • @Retention:表示需要在什么级别保存该注解信息

    取值 RetentionPolicy:

    • SOURCE:在源文件中有效,将被编译器丢弃
    • CLASS:在 class 文件中有效,将在类加载时丢弃(默认)
    • RUNTIME:始终不会丢弃,运行期也保留,可通过反射机制读取注解
  • @Documented:将注解包含在 Javadoc 中

  • @Inherited:允许子类继承父类的该注解

自定义注解实例

  • FruitName.java
/**
 * 水果名称注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}

  • FruitColor.java
/**
 * 水果颜色注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitColor {
	// 颜色枚举
    public enum Color{ BLUE,RED,GREEN};
    
	// 颜色属性
    Color fruitColor() default Color.GREEN;

}

  • FruitProvider.java
/**
 * 水果供应者注解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitProvider {
	// 供应商编号
    public int id() default -1;
    
	// 供应商名称
    public String name() default "";
    
    // 供应商地址
    public String address() default "";
}

  • FruitInfoUtil.java
/**
 * 注解处理器
 */
public class FruitInfoUtil {
    public static void getFruitInfo(Class<?> clazz){
        String strFruitName = " 水果名称:";
        String strFruitColor = " 水果颜色:";
        String strFruitProvicer = "供应商信息:";
        Field[] fields = clazz.getDeclaredFields();

        for(Field field :fields){
            if(field.isAnnotationPresent(FruitName.class)){
                FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
                strFruitName = strFruitName + fruitName.value();
                System.out.println(strFruitName);
            }
            else if(field.isAnnotationPresent(FruitColor.class)){
                FruitColor fruitColor = (FruitColor) field.getAnnotation(FruitColor.class);
                strFruitColor = strFruitColor 
                    + fruitColor.fruitColor().toString();
                System.out.println(strFruitColor);
            }
            else if(field.isAnnotationPresent(FruitProvider.class)){
                FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
                strFruitProvicer = " 供应商编号:" + fruitProvider.id()
                        + " 供应商名称:" + fruitProvider.name()
                        + " 供应商地址:" + fruitProvider.address();
                System.out.println(strFruitProvicer);
            }
        }
    }
}

  • Apple.java
/**
 * 注解使用
 */
public class Apple {    
    @FruitName("Apple")
    private String appleName;
    
    @FruitColor(fruitColor=Color.RED)
    private String appleColor;
    
    @FruitProvider(id = 1,name = "红富士",address = "红富士大厦")
    private String appleProvider;
    
    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }
    public String getAppleColor() {
        return appleColor;
    }
    
    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }
    public String getAppleName() {
        return appleName;
    }
    
    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }
    public String getAppleProvider() {
        return appleProvider;
    }
    
    public void displayName() {
        System.out.println("水果的名字是:苹果");
    }
}

  • ruitRun.java
public class FruitRun {
    public static void main(String[] args) {
        FruitInfoUtil.getFruitInfo(Apple.class);
    }
}

// 输出
/* 水果名称:Apple
 水果颜色:RED
 供应商编号:1 供应商名称:红富士 供应商地址:红富士大厦
 */

特性

Java 各版本的新特性

New highlights in Java SE 8

  1. Lambda Expressions
  2. Pipelines and Streams
  3. Date and Time API
  4. Default Methods
  5. Type Annotations
  6. Nashhorn JavaScript Engine
  7. Concurrent Accumulators
  8. Parallel operations
  9. PermGen Error Removed

New highlights in Java SE 7

  1. Strings in Switch Statement
  2. Type Inference for Generic Instance Creation
  3. Multiple Exception Handling
  4. Support for Dynamic Languages
  5. Try with Resources
  6. Java nio Package
  7. Binary Literals, Underscore in literals
  8. Diamond Syntax

Difference between Java 1.8 and Java 1.7?

Java 8 特性

Java 与 C++ 的区别

  • Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
  • Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
  • Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
  • Java 支持自动垃圾回收,而 C++ 需要手动回收。
  • Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
  • Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
  • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
  • Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

JRE or JDK

  • JRE is the JVM program, Java application need to run on JRE.
  • JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler “javac”

参考


  1. 有些方法看起来改变了对象,但是实质上是创建 了全新的对象并赋予其修改后的内容。 ↩︎

  2. args 有两个参数,第一个是待匹配字符串,第二个是匹配标志 ↩︎