JavaSE Note

本文最后更新于 2026年2月19日 晚上

快速入门

Java历史

Java EE > Java SE > Java ME

JDK: Java Development kit

JRE: Java Runtime Environment

JRE就是运行Java字节码的虚拟机。Java源码,要编译成Java字节码,就需要JDK,因为JDK除了包含JRE,还提供了编译器、调试器等开发工具。

JRE = JVM + Runtime Library

JDK = JRE + 编译器 调试器等

运行

假设写了一个Hello.java程序,里面有main方法

首先需要

javac Hello.java

这会产生Hello.class文件

然后运行

java Hello

注意这里传入的参数是类名,所以直接输入Hello

即可运行

Java程序基础

Java程序基本结构

类名要求:

  • 类名必须以英文字母开头,后接字母,数字和下划线的组合
  • 习惯以大写字母开头

注意到public是访问修饰符,表示该class是公开的。

不写public,也能正确编译,但是这个类将无法从命令行执行。

class内部,可以定义若干方法(method):

public class Hello {
    public static void main(String[] args) { // 方法名是main
        // 方法代码...
    } // 方法定义结束
}

方法名也有命名规则,命名和class一样,但是首字母小写。

特殊的注释:以/**开头,以*/结束,如果有多行,每行通常以星号开头:

/**
 * 可以用来自动创建文档的注释
 * 
 * @auther liaoxuefeng
 */
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

这种特殊的多行注释需要写在类和方法的定义处,可以用于自动创建文档。

变量与数据类型

基本数据类型

Java定义了以下几种基本数据类型:

  • 整数类型:byte,short,int,long
  • 浮点数类型:float,double
  • 字符类型:char
  • 布尔类型:boolean

byte一个字节,long8个字节,char两个字节,其他和cpp一样。

对于整型只有signed没有unsigned

int i3 = 2_000_000_000; // 加下划线更容易识别
long n1 = 9000000000000000000L; // long型的结尾需要加L

对于float需要加f

Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean表示为4字节整数。

Java的char类型除了可表示标准的ASCII外,还可以表示一个Unicode字符:

Java在内存中总是使用Unicode表示字符

// 字符类型
public class Main {
    public static void main(String[] args) {
        char a = 'A';
        char zh = '中';
    }
}

还可以直接用转义字符\u+Unicode编码来表示一个字符:

// 注意是十六进制:
char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65
char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013

引用类型

除了上述基本类型的变量,剩下的都是引用类型。

引用类型的变量类似于C语言的指针,它内部存储一个“地址”,指向某个对象在内存的位置。

常量

final修饰

var关键字

相当于cpp的auto

运算

byteshort类型进行移位时,会首先转换为int再进行位移。

存在整型提升。

强转:只支持C方式的强转。

字符串的运算

ava的编译器对字符串做了特殊照顾,可以使用+连接任意字符串和其他数据类型,这样极大地方便了字符串的处理。例如:

// 字符串连接
public class Main {
    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "world";
        String s = s1 + " " + s2 + "!";
        System.out.println(s); // Hello world!
    }
}

如果用+连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接:

// 字符串连接
public class Main {
    public static void main(String[] args) {
        int age = 25;
        String s = "age is " + age;
        System.out.println(s); // age is 25
    }
}

还有多行字符串,格式是:

"""
...
"""

具体的细节不重要。

Java的字符串除了是一个引用类型外,还有个重要特点,就是字符串不可变。

数组

定义一个数组类型的变量,使用数组类型“类型[]”,例如,int[]。和单个基本类型变量不同,数组变量初始化必须使用new int[5]表示创建一个可容纳5个int元素的数组。

Java的数组有几个特点:

  • 数组所有元素初始化为默认值,整型都是0,浮点型是0.0,布尔型是false
  • 数组一旦创建后,大小就不可改变。

可以用数组变量.length获取数组大小:

数组是引用类型

也可以在定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小。

int[] ns = new int[] { 68, 79, 91, 85, 62 };
// 或者
int[] ns = { 68, 79, 91, 85, 62 };

流程控制

格式化输出

占位符 说明
%d 格式化输出整数
%x 格式化输出十六进制整数
%f 格式化输出浮点数
%e 格式化输出科学计数法表示的浮点数
%s 格式化字符串

和C差不多,详细的可以问AI

和输出相比,Java的输入就要复杂得多。

我们先看一个从控制台读取一个字符串和一个整数的例子:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in); // 创建Scanner对象
        System.out.print("Input your name: "); // 打印提示
        String name = scanner.nextLine(); // 读取一行输入并获取字符串
        System.out.print("Input your age: "); // 打印提示
        int age = scanner.nextInt(); // 读取一行输入并获取整数
        System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
    }
}

输入

我们先看一个从控制台读取一个字符串和一个整数的例子:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in); // 创建Scanner对象
        System.out.print("Input your name: "); // 打印提示
        String name = scanner.nextLine(); // 读取一行输入并获取字符串
        System.out.print("Input your age: "); // 打印提示
        int age = scanner.nextInt(); // 读取一行输入并获取整数
        System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
    }
}

首先,我们通过import语句导入java.util.Scannerimport是导入某个类的语句,必须放到Java源代码的开头,后面我们在Java的package中会详细讲解如何使用import

然后,创建Scanner对象并传入System.inSystem.out代表标准输出流,而System.in代表标准输入流。直接使用System.in读取用户输入虽然是可以的,但需要更复杂的代码,而通过Scanner就可以简化后续的代码。

有了Scanner对象后,要读取用户输入的字符串,使用scanner.nextLine(),要读取用户输入的整数,使用scanner.nextInt()Scanner会自动转换数据类型,因此不必手动转换。

switch语句

总结:一个switch语句具有自己的返回值,每个case后面用->表示返回。

使用switch时,如果遗漏了break,就会造成严重的逻辑错误,而且不易在源代码中发现错误。从Java 12开始,switch语句升级为更简洁的表达式语法,使用类似模式匹配(Pattern Matching)的方法,保证只有一种路径会被执行,并且不需要break语句:

// switch
public class Main {
    public static void main(String[] args) {
        String fruit = "apple";
        switch (fruit) {
        case "apple" -> System.out.println("Selected apple");
        case "pear" -> System.out.println("Selected pear");
        case "mango" -> {
            System.out.println("Selected mango");
            System.out.println("Good choice!");
        }
        default -> System.out.println("No fruit selected");
        }
    }
}

注意新语法使用->,如果有多条语句,需要用{}括起来。不要写break语句,因为新语法只会执行匹配的语句,没有穿透效应。

很多时候,我们还可能用switch语句给某个变量赋值。例如:

int opt;
switch (fruit) {
case "apple":
    opt = 1;
    break;
case "pear":
case "mango":
    opt = 2;
    break;
default:
    opt = 0;
    break;
}

使用新的switch语法,不但不需要break,还可以直接返回值。把上面的代码改写如下:

// switch
public class Main {
    public static void main(String[] args) {
        String fruit = "apple";
        int opt = switch (fruit) {
            case "apple" -> 1;
            case "pear", "mango" -> 2;
            default -> 0;
        }; // 注意赋值语句要以;结束
        System.out.println("opt = " + opt);
    }
}

这样可以获得更简洁的代码。

大多数时候,在switch表达式内部,我们会返回简单的值。

但是,如果需要复杂的语句,我们也可以写很多语句,放到{...}里,然后,用yield返回一个值作为switch语句的返回值:

// yield
public class Main {
    public static void main(String[] args) {
        String fruit = "orange";
        int opt = switch (fruit) {
            case "apple" -> 1;
            case "pear", "mango" -> 2;
            default -> {
                int code = fruit.hashCode();
                yield code; // switch语句返回值
            }
        };
        System.out.println("opt = " + opt);
    }
}

数组的操作

使用Arrays.toString()可以快速获取数组内容,参数是数组名

标准库的排序:Arrays.sort(),参数是数组名

多维数组的定义:int [][] a

使用Java标准库的Arrays.deepToString()可以快速获取多维数组的内容

OOP

OOP基础

方法

一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中。

如果有局部变量和字段重名,那么局部变量优先级更高,就必须加上this

class Person {
    private String name;

    public void setName(String name) {
        this.name = name; // 前面的this不可少,少了就变成局部变量name了
    }
}

可变参数用类型...定义,可变参数相当于数组类型:

class Group {
    private String[] names;

    public void setNames(String... names) {
        this.names = names;
    }
}

但是与数组又有所不同,假如把String... names改写成String[] names,那么传参的时候需要自己构造{"a", "b"}这样的String[],而且可以传入null,而可变参数可以保证无法传入,因为传入0个参数时,接收到的实际值是一个空数组而不是null

一个构造方法可以调用其他构造方法,这样做的目的是便于代码复用。调用其他构造方法的语法是this(…)

class Person {
    private String name;
    private int age;

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

    public Person(String name) {
        this(name, 18); // 调用另一个构造方法Person(String, int)
    }

    public Person() {
        this("Unnamed"); // 调用另一个构造方法Person(String)
    }
}

继承

Java使用extends关键字来实现继承

子类自动获得了父类的所有公有字段,严禁定义与父类重名的字段!

在OOP的术语中,我们把Person称为超类(super class),父类(parent class),基类(base class),把Student称为子类(subclass),扩展类(extended class)。

super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName

在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();

如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

阻止继承

从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。

例如,定义一个Shape类:

public sealed class Shape permits Rect, Circle, Triangle {
    ...
}

上述Shape类就是一个sealed类,它只允许指定的3个类继承它。如果写:

public final class Rect extends Shape {...}

是没问题的,因为Rect出现在Shapepermits列表中。但是,如果定义一个Ellipse就会报错:

public final class Ellipse extends Shape {...}
// Compile error: class is not allowed to extend sealed class: Shape

原因是Ellipse并未出现在Shapepermits列表中。这种sealed类主要用于一些框架,防止继承被滥用。

向上转型

位于继承树上方的类,可以赋值一个下面的

Person p = new Student();

向下转型是不安全的,可能会报错。

为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型。

利用instanceof,在向下转型前可以先判断:

Person p = new Student();
if (p instanceof Student) {
    // 只有判断成功才会向下转型:
    Student s = (Student) p; // 一定会成功
}

在Java14之后可以写得更简洁

if(p isinstanceof Student s) {
	// 可以使用Student型的对象s
}

多态

覆写 override

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。

例如,在Person类中,我们定义了run()方法:

class Person {
    public void run() {
        System.out.println("Person.run");
    }
}

在子类Student中,覆写这个run()方法:

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override

多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。

最典型的应用场景就是在一个方法里面,方法的参数是Person p,但是传入的可以是各种各样的派生于Person的子类,调用的方法可能是签名相同的方法,但是由于多态,实际调用出来的效果不同,但是这对于这个方法而言是不需要在乎的。

覆写Object方法

  • toString():把instance输出为String
  • equals():判断两个instance是否逻辑相等;
  • hashCode():计算一个instance的哈希值。

调用super

调用父类的被覆写的方法,比如super.hello()

final

如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承。

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承。

抽象类

如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。

因为无法执行抽象方法,因此这个类也必须声明为抽象类(abstract class)。

稍微有点像cpp的virtual关键字。

接口

如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口:interface

在Java中,使用interface可以声明一个接口:

interface Person {
    void run();
    String getName();
}

所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。

当一个具体的class去实现一个interface时,需要使用implements关键字。

一个类可以实现多个interface

接口继承

一个interface可以继承自另一个interfaceinterface继承自interface使用extends,它相当于扩展了接口的方法。例如:

interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}

在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:

List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口

default方法

在接口里面用default关键字声明方法并做实现。

静态字段和方法

接口的静态字段

public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

事实上前面的修饰可以省略,变成:

public interface Person {
    // 编译器会自动加上public static final:
    int MALE = 1;
    int FEMALE = 2;
}

一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名

在定义class的时候,我们需要在第一行声明这个class属于哪个包。

小明的Person.java文件:

package ming; // 申明包名ming

public class Person {
}

包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系

包作用域

位于同一个包下面的类,不用任何修饰的字段和方法就属于包作用域。

同一个包的类,可以访问包作用域内的字段和方法,比如:

package hello;

public class Person {
    // 包作用域:
    void hello() {
        System.out.println("Hello!");
    }
}
package hello;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.hello(); // 可以调用,因为Main和Person在同一个包
    }
}

为了避免名字冲突,我们需要确定唯一的包名。推荐的做法是使用倒置的域名来确保唯一性。例如:

  • org.apache
  • org.apache.commons.log
  • com.liaoxuefeng.sample

子包就可以根据功能自行命名。

内部类

如果一个类定义在另一个类的内部,这个类就是Inner Class:

class Outer {
    class Inner {
        // 定义了一个Inner Class
    }
}

Inner Class的实例不能单独存在,必须依附于一个Outer Class的实例。

要实例化一个Inner,我们必须首先创建一个Outer的实例,然后,调用Outer实例的new来创建Inner实例:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // 实例化一个Inner

匿名类

实例化Runnable?看不懂,暂时代填坑。

classpath

classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索class

推荐在启动JVM的时候设置classpath,实际就是传参:

java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello

依照;为分割的次序依次找.class文件。

或者用简写:

java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello

缺省的情况下为.

不要把任何Java核心库添加到classpath中!JVM根本不依赖classpath加载核心库!

假设我们有一个编译后的Hello.class,它的包名是com.example,当前目录是C:\work,那么,目录结构必须如下:

C:\work
└─ com
   └─ example
      └─ Hello.class

运行这个Hello.class必须在当前目录下使用如下命令:

C:\work> java -cp . com.example.Hello

JVM根据classpath设置的.在当前目录下查找com.example.Hello,即实际搜索文件必须位于com/example/Hello.class。如果指定的.class文件不存在,或者目录结构和包名对不上,均会报错。

jar包

本质是一个.zip的压缩包,把后缀改成.jar而已可以包含很多.class文件,方便下载和使用。

class版本

高版本的JDK可以兼容低版本的JDK,低版本的JDK不可以兼容高版本的JDK; 低版本jvm运行高版本class文件会报linkageError错误;报NoSuchMethodError错误,代表当前版本jvm缺少运行该文件的库方法

模块

从Java 9开始引入的模块,主要是为了解决“依赖”这个问题。如果a.jar必须依赖另一个b.jar才能运行,那我们应该给a.jar加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar,这种自带“依赖关系”的class容器就是模块。

如何编写模块?如何运行模块?如何打包JRE?

模块还可以隔离代码的访问权限。

这一块感觉看得也云里雾里,待填坑

Java核心类

字符串和编码

String是一个引用类型。

内部是通过一个char[]数组表示的,因此,按下面的写法也是可以的:

String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});

因为String太常用了,所以Java提供了"..."这种字符串字面量表示方法。

Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]字段,以及没有任何修改char[]的方法实现的。

  • 判等:equals()

  • 忽略大小写比较:equalsIgnoreCase()

  • 是否包含子串:contains(),这个方法的参数是CharSequence而不是String,因为CharSequenceString实现的一个接口。

  • 找到子串第一次出现的下标:indexOf()

  • 找到子串最后一次出现的下标:lastIndexOf()

  • 判断子串的前缀是否是某个子串:startsWith(),比如"hello".startsWith("he") == true

  • 判断子串的后缀是否是某个子串:endsWith(),比如"hello".endsWith("lo") == true

  • 提取子串:"Hello".substring(2); // "llo""Hello".substring(2, 4); "ll"

  • 获得去除首尾空白字符的字符串:trim()strip(),前者只会去掉空格,\t\r\n,而后者还会去掉全角的空格。

  • 判空:isEmpty()(就是判长度是否为0)

  • 判是否空白:isBlank()

  • 依据字符替换子串:

    s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
  • 依据子串替换子串:

    s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
  • 正则表达式替换

    String s = "A,,B;C ,D";
    s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
  • 分割字符串:传入正则表达式

    String s = "A,B,C,D";
    String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
  • 拼接字符串:静态方法join(),用指定的字符串连接字符串数组:

    String[] arr = {"A", "B", "C"};
    String s = String.join("***", arr); // "A***B***C"
  • 格式化,分为formatted()方法和静态的format()方法:

    public class Main {
        public static void main(String[] args) {
            String s = "Hi %s, your score is %d!";
            System.out.println(s.formatted("Alice", 80));
            System.out.println(String.format("Hi %d, your score is %.2f!", "Bob", 90));
        }
    }

    占位符有很多,如果不知道用什么就用%s

  • 类型转换:静态方法valueOf(),这是一个重载方法,编译器会根据参数自动选择合适的方法:

    String.valueOf(123); // "123"
    String.valueOf(45.67); // "45.67"
    String.valueOf(true); // "true"
    String.valueOf(new Object()); // 类似java.lang.Object@636be97c

    而想要把字符串转换为其他类型,则需要使用各个类型的静态方法

    int n1 = Integer.parseInt("123"); // 123
    int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
    boolean b1 = Boolean.parseBoolean("true"); // true
    boolean b2 = Boolean.parseBoolean("FALSE"); // false
  • Stringchar[]之间的互转:

    char[] cs = "Hello".toCharArray(); // String -> char[]
    String s = new String(cs); // char[] -> String

    String的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。

StringBuilder

StringBuilder对象可以预分配缓冲区,在新增字符的时候不会创建新的临时对象:

StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
    sb.append(',');
    sb.append(i);
}
String s = sb.toString();

还可以进行链式操作:

var sb = new StringBuilder(1024);
sb.append("Mr").
    append("Bob");

这是因为append()方法返回this

注意:对于普通的字符串+操作,并不需要我们将其改写为StringBuilder,因为Java编译器在编译时就自动把多个连续的+操作编码为StringConcatFactory的操作。在运行期,StringConcatFactory会自动把字符串连接操作优化为数组复制或者StringBuilder操作。

StringJoiner

解决用分割符拼接字符串数组的需求。

String[] names = {"Alice", "Bob", "Cathy"};
var sj = new StringJoiner(","); //指定分隔符
for (String name : names) {
	sj.add(name);	
}
// sj.toString()

也可以指定开头和结尾,这是构造方法的重载

var sj = new StringJoiner(",", "Hello", "!"); // "Hello"开头,"!"结尾

String.join()

String还提供了一个静态方法join(),这个方法在内部使用了StringJoiner来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()更方便:

String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);

包装类型

Java核心库为每种基本类型都提供了对应的包装类型:

基本类型 对应的引用类型
boolean java.lang.Boolean
byte java.lang.Byte
short java.lang.Short
int java.lang.Integer
long java.lang.Long
float java.lang.Float
double java.lang.Double
char java.lang.Character

创建包装类型的时候不推荐用new,推荐使用静态方法valueOf()

Integer n1 = Integer.valueOf(100);
Integer n2 = Integer.valueOf("100");

因为Integer.valueOf()返回缓存的Integer实例,这节省了堆内存的开辟。

我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。

创建新对象时,优先选用静态工厂方法而不是new操作符

一句话总结: 使用 new 是你在命令电脑“搬一块新砖来”;使用静态工厂方法是你在跟仓库管理员说“我要一块这种规格的砖”,管理员会看仓库里有没有现成的,或者给你一块更好用的特制砖。

Auto Boxing and Auto Unboxing

因为intInteger可以互相转换:

int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();

所以,Java编译器可以帮助我们自动在intInteger之间转型:

Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()

这种直接把int变为Integer的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。

自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。

不变类

所有的包装类型都是不可变的

public final class Integer {
    private final int value;
}

判等

一定要用equals()方法,虽然对于Integer在小于128的情况下有优化。

进制转换

Integer类本身还提供了大量方法,例如,最常用的静态方法parseInt()可以把字符串解析成一个整数:

int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析

Integer还可以把整数格式化为指定进制的字符串

// Integer:
public class Main {
    public static void main(String[] args) {
        System.out.println(Integer.toString(100)); // "100",表示为10进制
        System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
        System.out.println(Integer.toHexString(100)); // "64",表示为16进制
        System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
        System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
    }
}

Java的包装类型还定义了一些有用的静态变量

// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)

处理unsigned

// Byte
public class Main {
    public static void main(String[] args) {
        byte x = -1;
        byte y = 127;
        System.out.println(Byte.toUnsignedInt(x)); // 255
        System.out.println(Byte.toUnsignedInt(y)); // 127
    }
}

JavaBean

JavaBean是一种符合命名规范的class,它通过gettersetter来定义属性。

把"读方法(getter)" + "写方法(setter)"称为属性。

JavaBean是一种规范。

枚举类

枚举类底层就是一堆静态实例化好了的对象常量。

enum的定义

enum Weekday {
    SUN, MON, TUE, WED, THU, FRI, SAT;
}

enum常量本身带有类型信息,只能和相同类型的enum常量进行比较和赋值。

enum的比较

虽然是引用类型,但是enum类型的每个常量在JVM中只有一个唯一的实例,所以可以用==进行比较。

enum类

是一个类,有如下特征:

  • 继承自java.lang.Enum且无法被继承
  • 只能定义出实例而无法new出一个实例(内部通过把构造函数设为私有来避免)
  • 定义的每个实例都是引用类型的唯一实例
  • 可以将enum类型用于switch语句

写一个这样的Color枚举类:

public enum Color {
    RED, GREEN, BLUE;
}

编译出来的内部原理是这样的:

public final class Color extends Enum {
    public static final Color RED = new Color();
    public static final Color GREEN = new Color();
    public static final Color BLUE = new Color();
    private Color() {}
}

enum类具有这些方法:

  • 返回常量名:name(),比如String s = Weekday.MON.name() // "MON"

  • 返回定义的常量的顺序:ordinal(),比如int n = Weekday.MON.ordinal() // 1。但是不能用ordinal()来进行int和枚举类的比较,因为改变顺序之后结果也会变得不同,解决方案是这样添加一个字段:

    enum Color {
        RED(0), BLUE(1), GREEN(2);
        public final int value;
        private Color(int value) {
            this.value = value;
        }
    }

    因为每个实例都是静态的,所以对于这个实例存在唯一的public final int value

  • 可以给enum类覆写toString()方法:

    enum Color {
        RED(0, "红"), BLUE(1, "蓝"), GREEN(2, "绿");
        public final int value;
        private final String chinese;
        private Color(int value, String chinese) {
            this.value = value;
            this.chinese = chinese
        }
        @Override
        public String toString() {
            return this.chinese;
        }
    }

记录类

一个不变类具有以下特点:

  1. 定义class时使用final,无法派生子类;
  2. 每个字段使用final,保证创建实例后无法修改任何字段。
  3. 还需要正确覆写equals()hashCode()方法,这样才能在集合类中正常使用。

record类的定义

Java14开始引入,用关键字record声明:

record Point(int x, int y) {}

等价于:

代码JAVA · 19 行
final class Point extends Record {
    private final int x;
    private final int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public int x() {return this.x;}
    public int y() {return this.y;}
    public String toString() {
        return String.format("Point[x=%s, y=%s]", x, y);
    }
    public boolean equals(Object o) {
        ...
    }
    public int hashCode() {
        ...
    }
}

使用record关键字,可以一行写出一个不变类。

构造方法

假设Point类的xy不允许负数,我们就得给Point的构造方法加上检查逻辑:

public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException();
        }
    }
}

后面的this.x = x; this.y = y;不需要写,编译器会自动生成,只需要写检查逻辑即可。

可以添加静态方法。一种常用的静态方法是of()方法,用来创建Point

public record Point(int x, int y) {
    public static Point of() {
        return new Point(0, 0);
    }
    public static Point of(int x, int y) {
        return new Point(x, y);
    }
}

BigInteger

高精度大整数。

BigInteger bi = new BigInteger("123456789123456789");

使用实例方法进行运算(add()等)。

BigIntegerIntegerLong一样,也是不可变类,并且也继承自Number类。因为Number定义了转换为基本类型的几个方法:

  • 转换为bytebyteValue()
  • 转换为shortshortValue()
  • 转换为intintValue()
  • 转换为longlongValue()
  • 转换为floatfloatValue()
  • 转换为doubledoubleValue()

因此,通过上述方法,可以把BigInteger转换成基本类型。

如果BigInteger表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()longValueExact()等方法,在转换时如果超出范围,将直接抛出ArithmeticException异常。

BigDecimal

高精度小数。

BigDecimal bd = new BigDecimal("123.4567");

scale()方法可以求小数的位,如果一个BigDecimalscale()返回负数,例如,-2,表示这个数是个整数,并且末尾有2个0。

stripTrailingZeros()方法可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal

可以对一个BigDecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:

BigDecimal d1 = new BigDecimal("123.456789");
BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568

在做除法的时候需要指定精度和截断方式,避免除不尽的情况:

BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入
BigDecimal d4 = d1.divide(d2); // 报错:ArithmeticException,因为除不尽

还可以对BigDecimal做除法的同时求余数:

BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102
System.out.println(dr[1]); // 0.105

比较的时候,用compareTo()方法。原因:equals()方法同时要求scale相同。

实际上一个BigDecimal是通过一个BigInteger和一个scale来表示的,即BigInteger表示一个完整的整数,而scale表示小数位数:

public class BigDecimal extends Number implements Comparable<BigDecimal> {
    private final BigInteger intVal;
    private final int scale;
}

常用工具类

Math

Math.abs(), Math.max(), Math.min(), Math,pow()...

SecureRandom

用来创建安全的随机数的,具体用法感觉问LLM就行。

异常

Java的异常简介

graph BT
    %% 定义节点
    Object[Object]
    Throwable[Throwable]
    Error[Error]
    Exception[Exception]
    OOM[OutOfMemoryError]
    RE[RuntimeException]
    IOE[IOException]
    NPE[NullPointerException]
    IAE[IllegalArgumentException]
    OtherError[...]
    OtherExc[...]

    %% 定义继承关系
    Throwable --> Object
    Error --> Throwable
    Exception --> Throwable
    
    OOM --> Error
    OtherError --> Error
    
    RE --> Exception
    IOE --> Exception
    OtherExc --> Exception
    
    NPE --> RE
    IAE --> RE

    %% 样式美化(可选)
    style Object fill:#f9f,stroke:#333,stroke-width:2px
    style Throwable fill:#fff4dd,stroke:#d4a017,stroke-width:2px
    style Error fill:#fbb,stroke:#a00
    style Exception fill:#bbf,stroke:#00a

Error表示严重错误,程序对此无能为力,比如内存耗尽或者栈溢出;

Exception表示运行时错误,可以被捕获并处理

Exception又分为两大类:

  1. RuntimeException以及它的子类;
  2. RuntimeException(包括IOExceptionReflectiveOperationException等等)

Java规定:

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception
  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。

编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析。

捕获异常

try {
    // 可能发生异常的代码
} catch(Exception e) {
    // 处理异常
}

在方法定义的时候,使用throws Xxx表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。

如果调用了一个抛出异常的方法却不想处理,可以在调用这个方法的方法的签名处写上throws Xxx,这样把锅甩到了更高层。

由此可见,只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在main()方法中捕获,不会出现漏写try的情况。这是由编译器保证的。main()方法也是最后捕获Exception的机会。

如果不想写任何try代码,可以直接把main()方法定义为throws Exception,代价就是一旦发生异常,程序会立刻退出。

所有异常都可以调用printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。

捕获异常

多catch语句

try {
    process1();
    process2();
    process3();
} catch (IOException e) {
    System.out.println(e);
} catch (NumberFormatException e) {
    System.out.println(e);
}

JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

catch的顺序非常重要:子类必须写在前面。

finally语句

finally语句块保证有无错误都会执行。

try {
    process1();
    process2();
    process3();
} catch (UnsupportedEncodingException e) {
    System.out.println("Bad encoding");
} catch (IOException e) {
    System.out.println("IO error");
} finally {
    System.out.println("END");
}

如果没有发生异常,就正常执行try { ... }语句块,然后执行finally。如果发生了异常,就中断执行try { ... }语句块,然后跳转执行匹配的catch语句块,最后执行finally

可见,finally是用来保证一些代码必须执行的。

捕获多种异常

如果处理不同的异常的逻辑相同,可以使用|连在一起:

try {
    process();
} catch (IOException | NumberFormatException e) {
    // IOException或NumberFormatException
    System.out.println("Bad input");
}

抛出异常

异常的传播

通过printStackTrace()可以打印出异常的传播路径

抛出异常

分为两步:

  1. 创建某个Exception的实例;
  2. throw语句抛出。
void process(String s) {
    if (s == null) {
        throw new NullPointerException();
    }
}

捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!

所谓的“留住原始异常”,指的就是传参可以传进去:

void process1() {
    try {
        process2();
    } catch (NullPointerException e) {
        throw new IllegalArgumentException(e); // HERE
    }
}

void process2() {
    throw new NullPointerException();
}

这样在printStackTrace()的时候可以更加清晰的限制异常的根本原因。

catch中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常。

异常屏蔽

如果在执行finally语句时抛出异常,原来在catch中准备抛出的异常就“消失”了,因为只能抛出一个异常。没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)。

解决方案:先暂存原始异常,使用Throwable.addSuppressed()把原始异常添加进来,最后在finally抛出:

Exception origin = null;
try {
	System.out.println(Integer.parseInt("abc"));
} catch (Exception e) {
    origin = e;
    throw e;
} finally {
    Exception e = new IllegalArgumentException();
    if (origin != null) {
        e.addSuppressed(origin);
    }
    throw e;
}

这样异常栈里面两个异常都能看见。

自定义异常

Java标准库定义的常用异常包括:

Exception
├─ RuntimeException
│  ├─ NullPointerException
│  ├─ IndexOutOfBoundsException
│  ├─ SecurityException
│  └─ IllegalArgumentException
│     └─ NumberFormatException
├─ IOException
│  ├─ UnsupportedCharsetException
│  ├─ FileNotFoundException
│  └─ SocketException
├─ ParseException
├─ GeneralSecurityException
├─ SQLException
└─ TimeoutException

自定义的BaseException应该提供多个构造方法:

public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}

使用断言

断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;

assert x > 0 : "x must > 0";

对可恢复的错误不能使用断言,而应该抛出异常;

断言很少被使用,更好的方法是编写单元测试。

日志

待填坑,感觉就是一堆API

使用JDK Logging

一个Java自带的日志系统,具体的API要用的时候再查吧。

import java.util.logging.Level;
import java.util.logging.Logger;

使用Commons Logging

Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。

使用Log4j

使用SLF4J和Logback

反射

Java的反射是指程序在运行期可以拿到一个对象的所有信息,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

这是因为Java传参是可以传父类甚至接口的。

Class类

除了int等基本类型外,Java的其他类型全部都是class(包括interface

JVM在执行过程中,每遇到(意味着是遇到再加载的,而不是编译就加载完了,这叫动态加载)一种类型(比如自己定义的class)就会新建一个这样的静态实例保存在内存中:

public final class Class {
	private Class() {}
    // ...字段略
}

注:这里的Class是一个名为Classclass

也就是说,有一个名为Class的类型,里面会存这个类型的各种信息。

就举个例子吧,当这个程序里面遇到String类的时候,JVM首先读取String.class到内存中,然后创建一个Class实例:Class cls = new Class(String);,这个操作只有JVM能做。

一言以蔽之,JVM持有很多Class实例,每一个Class实例都指向一个具体的数据类型,一个Class实例中包含了这个类型的所有信息,比如类名、包名、父类、实现的接口等等。

通过一个Class实例,获取这个类型的所有信息的方法,叫做反射。

获取一个classClass实例的三个方法:

  1. Class cls = String.class;
  2. String s = ""; Class cls = s.getClass();
  3. Class cls = Class.forName("java.lang.String");

由于一个类型在JVM的内存中对应唯一的一个Class 实例,可以使用==来比较。

这个时候我们需要区分Class之间用==的比较,和isinstanceof之间的关系:前者要求严格类型匹配一致,后者只要是子类也可以。

如何从Class实例获取信息?getName(), getSimpleName, getPackage(), isInterface(), isEnum(),isArray(), isPrimitive()

注意:数组和初始类型也有对应的Class实例,比如String[],其类名是Ljava.lang.Stringint.class也可以访问。

有了Class实例,就可以通过这个Class实例来创建对应类型的实例:

Class cls = String.class;
String s = (String) cls.newInstance();

但是有局限性:只能调用public的无参数构造方法

访问字段

字段用Field对象来表示,可以通过Class实例获取Field对象,有这么几种方式:

  • Field getField(name):根据字段名获取某个public的field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getFields():获取所有publicfield(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

对于一个Field对象,有这么几种方式获取字段的信息:

  • getName():返回字段名称,例如,"name"

  • getType():返回字段类型,也是一个Class实例,例如,String.class

  • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。

    int m = f.getModifiers();
    Modifier.isFinal(m); // true
    Modifier.isPublic(m); // false
    Modifier.isProtected(m); // false
    Modifier.isPrivate(m); // true
    Modifier.isStatic(m); // false

可以通过反射拿到一个实例对应的字段的值:

Object p = new Person("Alice");
Class c = p.getClass();
Field f = p.getDeclaredField("name");
Object value = f.get(p); // value.equals("Alice")

如果这是一个私有字段,无法访问,可以通过

f.setAccessible(true);

来强制进行访问。

同时,可以设置字段的值,通过Field.set(Object, Object)实现,其中第一个Object参数是指定的实例,第二个Object参数是待修改的值:

修改非public字段,需要首先调用setAccessible(true)

调用方法

和上面说的访问字段相对应的,就是获取方法,我们有一个类Method,和Field的生态位相同,获取Method的方法如下:

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类),这里的class ...是这个方法的参数。
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

一个Method对象包含一个方法的所有信息:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。

调用方法,,使用invoke,传入的第一个参数是对象实例,即在哪个实例上调用该方法,而如果是静态方法,第一个参数传null。可以参考示例代码:

String s = "Hello, world";
Method m = String.class.getMethod("substring", int.class);
String r = (String) m.invoke(s, 6);

注意到substring()有两个重载方法,我们获取的是String substring(int)这个方法。

调用非public 方法,可以通过Method.setAccessible(true)允许其调用。

多态

一个Person类定义了Hello()方法,其子类Student也覆写了Hello()方法,那么,从Person.class获取的Method,作用于Student实例时,调用的是Student类覆写的Hello()方法。

调用构造方法

由于newInstance()有局限,只能调用该类的public无参构造方法:

Person p = Person.class.newInstance();

Java的反射API提供了Constructor对象,包含构造方法的所有信息,可以创建一个实例。

Constructor对象和Method对象很相似。

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用Constructor,使用newInstance(...),参数就是对应的构造函数的参数。

调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。

获取继承关系

获取父类的Class

Class对象使用getSuperclass()方法即可。

获取interface

Class[] is = cls.getInterfaces();

要特别注意:getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口。

对所有interfaceClass调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces()

如果一个类没有实现任何interface,那么getInterfaces()返回空数组。

继承关系

如果是两个对象,判断是否是某个类型,或者向上转型是否成立时,可以使用isinstanceof运算符;

如果是两个Class实例,进行同样的判断,可以使用isAssignableFrom()

Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object

参数里面是派生类或者是自己。

动态代理

不实现接口,却直接在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。

动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

具体的语法有点复杂,我觉得问LLM就好,待填坑。

注解

使用注解

注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,比如@Override, @PostConstruct等。

注释会被编译器直接忽略,注解则可以被编译器打包进入class文件。

注解的作用

第一类:由编译器使用的注解,比如

  • @Override:让编译器检查该方法是否正确地实现了覆写;
  • @SuppressWarnings:告诉编译器忽略此处代码产生的警告。

这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。

第二类:由底层库使用的,一般不需要我们自己处理。

第三类:在运行期能够被读取的注解,一直存在于JVM中,这是最常用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。

定义一个注解时,还可以定义配置参数,这个参数必须是常量。

注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。

举例:

public class Hello {
    @Check(min=0, max=100, value=55)
    public int n;

    @Check(value=99)
    public int p;

    @Check(99) // @Check(value=99)
    public int x;
    // 大部分注解会有一个名为value的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。

    @Check
    public int y;
}

定义注解

Java语言使用@interface语法来定义注解,它的格式如下:

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐)。最常用的参数应当命名为value

元注解

有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。

什么叫“修饰其他注解”?就是用元注解,来给自己写的注解,划定一些规范。

@Target

valueElementType[]数组,只有一个元素时,可以省略数组的写法,如果有多个元素,可以写成类似@Target({Element.FIELD, Element.METHOD})的形式。

所谓ElementType,是一个枚举类,有这么几种:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 方法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 方法参数:ElementType.PARAMETER

使用举例:定义注解@Report可用在方法上,我们必须添加一个@Target(ElementType.METHOD)

@Target(ElementType.METHOD)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

@Retention

定义注解的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE
  • 仅class文件:RetentionPolicy.CLASS
  • 运行期:RetentionPolicy.RUNTIME

缺省为CLASS,但是一般使用是RUNTIME

@Repeatable

经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Report注解:

@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}

@Inherited

使用@Inherited定义子类是否可继承父类定义的Annotation

@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效。

如何定义Annotation

第一步,用@interface定义注解:

public @interface Report {
}

第二步,添加参数、默认值:

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。

第三步,用元注解配置注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

其中,必须设置@Target@Retention@Retention一般设置为RUNTIME,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited@Repeatable

处理注解

根据@Retention的配置:

  • SOURCE类型的注解在编译期就被丢掉了;
  • CLASS类型的注解仅保存在class文件中,它们不会被加载进JVM;
  • RUNTIME类型的注解会被加载进JVM,并且在运行期可以被程序读取。

如何使用注解完全由工具决定。SOURCE类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。只有RUNTIME类型的注解不但要使用,还经常需要编写。

由于注解也是一种class,所有的注解都继承自java.lang.annotation.Annotation,所以读取注解需要使用反射API,这些反射API均适用于ClassFieldMethodConstructor(但是有些情况下存在区别,会有说明):

判断某个注解是否存在:使用isAnnotationPresent()方法,比如判断@Report是否存在于Person类:

Person.class.isAnnotationPresent(Report.class);

读取Annotation:使用getAnnotation(Class)

Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();

如果Annotation不存在,将返回null

读取方法、字段和构造方法的Annotation和Class类似,但是对于方法参数来说,比较复杂,因为方法参数,可以看成一个数组,然后每个参数,又可以定义多个注解,举个例子:

对于这么一个方法:

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

首先获取这个方法的Method实例,然后获取所有参数的Annotations:

Method m = ...;
Annotation[][] annos = m.getParameterAnnotations();

然后获取第一个参数(索引为0)的所有Annotation:

Annotation[] annoOfName = annos[0];

然后遍历annoOfName,从而读取所有注解。

使用注解

注解如何使用,完全由程序自己决定。

举个例子,我写一个注解Range,是用来规定String长度范围的:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int min() default 0;
    int max() default 255;
}

然后假设我在一个类里面用了这个注解:

class Person {
    @Range(min=1, max=20)
    public String name;
}

定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。这里我们写一个检查的方法:

void check(Person p) throws IllegalArgumentException, ReflectiveOperationException {
	for (Field f : p.getClass().getFields()) {
		Range range = field.getAnnotation(Range.class);
        if (range != null) {
            Object value = field.get(p);
            if (value isinstanceof String) {
                if (s.length() < range.min() || s.length > range.max()) {
                    throw new IllegalArgumentException("Invalid field: " + field.getName());
                }
            }
        }
	}
}

泛型

什么是泛型

其实和cpp差不多吧,这个概念本身没什么好说的。

这里注意一下向上转型的问题,ArrayList<T>是可以向上转型为List<T>的,但是不能ArrayList<Integer>向上转型为ArrayList<Number>List<Number>

使用泛型

在使用ArrayList的时候,假如不使用泛型,所谓的T就会是Object

List list = new ArrayList();
list.add("Hello");
String first = (String) list.get(0);

但如果定义泛型类型之后,类型也会强定义。

编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型,比如

List<String> list = new ArrayList<String>();

可以简写为

List<String> list = new ArrayList<>();

接口泛型

Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

public interface Comparable<T> {
    /**
     * 返回负数: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回正数: 当前实例比参数o大
     */
    int compareTo(T o);
}

这里检测是否实现接口,是用反射实现的,isAssignableFrom()

编写泛型

单纯写的话没什么好说的,和cpp道理差不多。

静态方法

泛型类型<T>不能用于静态方法,但是可以单独为静态方法改写为另一种泛型类型:

public class A<T> {
    private T a;
    public A(T a) {
        this.a = a;
    }
    public static <K> A<K> create(K a) {
        return new A<K>(a);
    }
}

其实这里声明写成public static <T> A<T> create(T a)也是可以的,但是这个T和上面的T没有任何关系,用不同的名字标识才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

多个泛型

比如

public class Map <K, V> {}

擦拭法

所谓“擦拭法”,就是Java语言实现泛型的方式。

所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

简而言之,虚拟机眼里看到的代码,所有的T全都是Object,编译器根据<T>实现安全的强制转型。

由此可见,Java的泛型有这么几个局限:

  1. <T>不能是基本类型,所以我们才有包装类如Integer

  2. 无法取得带泛型的Class对象(反射里面的),所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>

  3. 无法判断带泛型的类型:只能判断p isinstanceof Pair而不能判断p isinstanceof Pair<String>

  4. 不能在代码里面实例化泛型

    public class Pair<T> {
        ...
        first = new T(); // compile error
    }

    因为擦拭之后会变成new Object(),那么对于任何的T都是如此,是不合理的,所以编译器会做出阻止。

    但是可以借助反射实现T的实例化:

    public class Pair<T> {
        private T first;
        private T last;
        public Pair(Class<T> clazz) {
            first = clazz.getDeclaredConstructor().newInstance();
            last = clazz.getDeclaredConstructor().newInstance();
        }
    }

    在使用的时候,传入Class<T>

    Pair<String> pair = new Pair<>(String.class);

    注,这里的Class<T>,其实是因为Class类本身也是一个支持泛型的类,比如String.class实际上就是Class<String>

泛型继承

一个类可以继承自一个泛型类,比如这样写:

public class IntPair extends Pair<Integer> {}

前面讲了,我们无法获取Pair<T>T类型,即给定一个变量Pair<Integer> p,无法从p中获取到Integer类型。

这里解释一下,其实就是由于擦拭法,运行期只存在Pair而不存在Pair<Integer>。当然,也可以用反射Class的角度去理解。

但是,在父类是泛型类型的情况下,编译器就必须把类型T(对IntPair来说,也就是Integer类型)保存到子类的class文件中,不然编译器就不知道IntPair只能存取Integer这种类型。

在继承了泛型类型的情况下,子类可以获取父类的泛型类型:

Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t isinstanceof ParameterizedType) {
    ParameterizedType pt = (ParameterizedType) t;
    Type[] types = pt.getActualTypeArguments();
    Type firstType = types[0];
    Class<?> typeClass = (Class<?>) firstType;
    System.out.println(typeClass); // Integer
}

看不懂的话,待填坑。

extends通配符

假设我们写一个静态方法,接收参数Pair<Number>

public static int add(Pair<Number> p) {
    return p.getFirst().intValue() + p.getSecond().intValue();
}

然后,这样使用:

add(new Pair<Number>(1, 2));

是可以编译的,因为虽然写的是Number,实际传入的1和2都是Integer

那么,假设我们这样使用:

add(new Pair<Integer>(1, 2));

却无法编译了,这是因为类型擦除,Pair<Integer>不是Pair<Number>的子类。

但是我们却有传入其“子类”的需求,所以我们引入extends通配符

public static int add(Pair<? extends Number> p) {
	return p.getFirst().intValue() + p.getSecond().intValue();
}

使得方法接收所有泛型类型为NumberNumber子类的Pair类型。

这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。

关于类型为<? extends Number>的赋值之类的要求(下面用X简称):只能读取,不能写。

具体而言,假如有一个方法签名为Pair<? extends Number> getFirst(),那么是可以安全使用的;如果方法签名是Pair<? extends Number> setFirst()那么是不能安全使用的。

这里null是例外,传入是可以的。

举个例子吧,假设本来是Double,然后我传入的是Integer,这两者都满足<? extends Number>,但是事实上无法兼容。

extends通配符的作用

比如定义一个这样的方法:

int sumOfList(List<? extends Integer> list) {
    int sum = 0;
    for (int i=0; i<list.size(); i++) {
        Integer n = list.get(i);
        sum = sum + n;
    }
    return sum;
}

通过使用extends通配符,阻止了对list使用set方法,表明了这个方法只读不改。

(恶意调用set(null)除外)

还可以使用extends限定T类型。

super通配符

就是和extends通配符相反,可以接受这个类型以及这个类型的父类。

使用<? super Integer>通配符表示:

  • 允许调用set(? super Integer)方法传入Integer的引用;
  • 不允许调用get()方法获得Integer的引用。

唯一例外是可以获取Object的引用:Object o = p.getFirst()

换句话说,使用<? super Integer>通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。

对比extends通配符,发现一个是只能读不能写,一个是只能写不能读,传入null和获取Object除外。

举个例子:

public class Collections {
    // 把src的每个元素复制到dest中:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

通过不同的通配符,限制了读和写。

另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加。

PECS原则

用来判断何时使用extends通配符,何时使用super通配符。

Producer Extends Consumer Super

如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。

无限定通配符

比如:

void sample(Pair<?> p) {
}

不能读也不能写,只能做一些null判断。

<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类。

泛型和反射

Java的部分反射API也是泛型。

比如Class<T>就是泛型,Class<String>等价于String.class

构造方法也是泛型。

我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组:

Pair<String>[] ps = null; // ok
Pair<String>[] ps = new Pair<String>[2]; // compile error!

必须通过强制转型实现带泛型的数组:

@SuppressWarnings("unchecked")
Pair<String>[] ps = (Pair<String>[]) new Pair[2];

不能直接创建泛型数组T[],因为擦拭后代码变为Object[]

// compile error:
public class Abc<T> {
    T[] createArray() {
        return new T[5];
    }
}

必须借助Class<T>来创建泛型数组:

T[] createArray(Class<T> cls) {
    return (T[]) Array.newInstance(cls, 5);
}

不要(谨慎)使用泛型可变参数,返回参数来创造泛型可变数组,乃至于直接谨慎使用泛型可变参数。

本质还是擦拭法,在JVM看来都是Object[]

集合

Java集合简介

java.util包主要提供了三种类型的集合:List, Set, Map

Collection接口是除Map外所有其他集合类的根接口。

一小部分集合类是遗留类,不应该继续使用:

  • Hashtable:一种线程安全的Map实现;
  • Vector:一种线程安全的List实现;
  • Stack:基于Vector实现的LIFO的栈。

还有一小部分接口是遗留接口,也不应该继续使用:

  • Enumeration<E>:已被Iterator<E>取代。

使用List

考察List<E>接口,可以看到几个主要的接口方法:

  • 在末尾添加一个元素:boolean add(E e)
  • 在指定索引添加一个元素:boolean add(int index, E e)
  • 删除指定索引的元素:E remove(int index)
  • 删除某个元素:boolean remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(包含元素的个数):int size()

ArrayListLinkedList

List允许添加null

也可以使用of方法快速创建List

List<Integer> list = List.of(1, 2, 5);

但是这样创建的既不是ArrayList也不是LinkedList,而是一个不可变的短数组,内部是顺序实现的。

遍历list

可以使用for(int i = 0; i < n; i++)的形式遍历,但是这个只对ArrayList高效。

坚持使用迭代器Iterator来访问Listfor-each循环内部也是用的Iterator

List和Array之间的转换

List变成Array有三种方法:

  1.  List<String> list = List.of("apple", "pear", "banana");
     Object[] array = list.toArray();
     // 由于会损失类型信息所以应用很少
     
    
    2. ```java
        List<String> list = List.of("apple", "pear", "banana");
        String[] array = list.toArray(new String[3]);
  2. 函数式的写法,后面介绍,待填坑。

Array变为List:使用List.of(...)方法。

编写equals方法

满足自反性、对称性、传递性、一致性、以及对于null始终返回false

使用Map

put, contains, get

Map来说,要遍历key可以使用for each循环遍历Map实例的keySet()方法返回的Set集合:

Map<Integer, String> map = new HashMap<>();
...
for(Integer k : map.keySet()) {
    String v = map.get(k);
}

同时遍历keyvalue可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射:

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println(key + " = " + value);
}

编写hashCode方法

这个其实没啥好讲的,CS61B学过不少了。

使用EnumMap

如果作为key的对象是enum类型,还可以使用Java集合库提供的一种EnumMap。它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。

举个例子:

public class Main {
    public static void main(String[] args) {
        Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
        map.put(DayOfWeek.MONDAY, "星期一");
        map.put(DayOfWeek.TUESDAY, "星期二");
        map.put(DayOfWeek.WEDNESDAY, "星期三");
        map.put(DayOfWeek.THURSDAY, "星期四");
        map.put(DayOfWeek.FRIDAY, "星期五");
        map.put(DayOfWeek.SATURDAY, "星期六");
        map.put(DayOfWeek.SUNDAY, "星期日");
        System.out.println(map);
        System.out.println(map.get(DayOfWeek.MONDAY));
    }
}

使用TreeMap

内部对Key进行排序的Map,将实现接口SortedMap,而TreeMap就是这样一种Map

放入的Key必须实现Comparable接口,或者在创建SortedMap的时候同时指定一个自定义排序算法(传入一个自己实现的Compatator接口):

public class Main {
    public static void main(String[] args) {
        Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
            public int compare(Person p1, Person p2) {
                return p1.name.compareTo(p2.name); // compareTo是Comparable接口的方法
            }
        })
    }
}

class Person {
	public String name;
    public int age;
}

Comparator 是一个接口:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

这个地方传入的是一个匿名内部类。

注意到这里Key没有覆写equals()方法,是因为TreeMap的比较不依赖于此,而是依赖于compare方法或者compareTo

实现compareTo或者compare方法

如果a<b,返回负数,如果a>b,返回正数,如果a.equals(b),返回0

使用Properties

Java默认配置文件以.properties为扩展名,每行以key=value表示,可以用Map<String, String>来表示它,因为配置文件非常常用,所以Java集合库提供了一个Properties来表示一组“配置”。

# setting.properties

last_open_file=/data/hello.txt
auto_save_interval=60

使用范例:

String f = "setting.properties";
Properties props = new Properties();
props.load(new java.io.FileInputStream(f));

String filepath = props.getProperty("last_open_file");
String interval = props.getProperty("auto_save_interval", "120");
props.setProperty("language", "Java");

使用Set

  • 将元素添加进Set<E>boolean add(E e)
  • 将元素从Set<E>删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)

使用Queue

队列接口Queue定义了以下几个方法:

  • int size():获取队列长度;
  • boolean add(E)/boolean offer(E):添加元素到队尾;
  • E remove()/E poll():获取队首元素并从队列中删除;
  • E element()/E peek():获取队首元素但并不从队列中删除。

注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。我们用一个表格总结如下:

throw Exception 返回false或null
添加元素到队尾 add(E e) boolean offer(E e)
取队首元素并删除 E remove() E poll()
取队首元素但不删除 E element() E peek()

注意:不要把null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。

Queue是一个接口,实现这个接口的类是LinkedList

使用PriorityQueue

方法和Queue一致。

不是接口而是类,直接new PriorityQueue<>()即可。

但是传入的元素要么实现Comparable接口,要么创建PriorityQueue的时候传入一个Comparator接口。

这里总是默认小根堆,较小的调用remove或者poll会优先出来。

使用Deque

Queue Deque
添加元素到队尾 add(E e) / offer(E e) addLast(E e) / offerLast(E e)
取队首元素并删除 E remove() / E poll() E removeFirst() / E pollFirst()
取队首元素但不删除 E element() / E peek() E getFirst() / E peekFirst()
添加元素到队首 addFirst(E e) / offerFirst(E e)
取队尾元素并删除 E removeLast() / E pollLast()
取队尾元素但不删除 E getLast() / E peekLast()

Deque接口扩展自Queue!这与C++的std::queue继承自std::deque相反。

Deque是一个接口,实现这个接口的类有ArrayDequeLinkedList

使用Stack

事实上Java没有Stack接口,只能用Deque来模拟Stack:

  • 把元素压栈:push(E)/addFirst(E)
  • 把栈顶的元素“弹出”:pop()/removeFirst()
  • 取栈顶元素但不弹出:peek()/peekFirst()

使用Iterator

使用for-each循环的基础就在于集合类实现了Iterable接口,该接口通过调用iterator()方法返回一个Iterator 对象。

Iterator对象,就是实现了Iterator接口的对象,这个接口要求实现next()hasNext()方法。

使用Collections

提供了一系列方法。

Collections.sort(),可以对List进行排序,必须传入可变ListCollections.shuffle(),传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌。

Collections还提供了一组方法把可变集合封装成不可变集合:

  • 封装成不可变List:List<T> unmodifiableList(List<? extends T> list)
  • 封装成不可变Set:Set<T> unmodifiableSet(Set<? extends T> set)
  • 封装成不可变Map:Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)

IO

读写文件的内容,待填坑。

日期与时间

感觉就是一堆API,待填坑

单元测试

编写JUnit测试

测试驱动开发:是指先编写接口,紧接着编写测试。编写完测试后,我们才开始真正编写实现代码。

package com.itranswarp.learnjava;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class FactorialTest {

    @Test
    void testFact() {
        assertEquals(1, Factorial.fact(1));
        assertEquals(2, Factorial.fact(2));
        assertEquals(6, Factorial.fact(3));
        assertEquals(3628800, Factorial.fact(10));
        assertEquals(2432902008176640000L, Factorial.fact(20));
    }
}

正则表达式

待填坑

加密与安全

待填坑

多线程

多线程是Java最基本的一种并发模型。