很多都和 C 语言一样吧,这里记录一下和 C 的区别:

# 关于引用

  • 没有指针,什么都是引用:数组元素是引用
  • 判断引用的内容是否相等,要用 a.equals(b)null.equals(a) 会报错,而 a.equals(null) 不会报错。
  • 基本类型参数的传递,是传值;其他都是传引用。

# I/O 输入与输出

# 输出

System.out.print()      // 不带换行
System.out.println()    // 自带换行
System.out.printf()     // 格式化输出,用法同 C

# 输入

import java.util.Scanner;

Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
int age = scanner.nextInt();

# switch 语法和表达式 (Java 12)

Java 11 及之前的 switch 都是类似 C 的语法:

switch (fruit) {
case "apple":
    System.out.println("Selected apple");
    break;
case "pear":
    System.out.println("Selected pear");
    break;
case "mango":
    System.out.println("Selected mango");
    break;
default:
    System.out.println("No fruit selected");
    break;
}

Java 12 以后,switch 语句支持新语法,这种语法用到了像是 lambda 表达式的 ->,且不需要 break,非常简洁:

int option = 1;
switch (option) {
    case 1 -> System.out.println("Selected 1");
    case 2 -> System.out.println("Selected 2");
    case 3 -> {
        // 可以接大括号,做更复杂的事情
        // other things...
        System.out.println("Selected 3");
    }
    default -> System.out.println("Not selected");
}

switch 语句的基础上,又衍生出了 switch 表达式,每个分支都是一个表达式,最后的值就是 switch 表达式的值:

int option = 1;
String str = switch (option) {
    case 1 -> "Selected 1";
    case 2 -> "Selected 2";
    case 3 -> {
        // 也可以接大括号,做更复杂的事情
        System.out.println("User Selected 3");
        // 使用 yield 返回
        yield "Selected 3";
    }
    default -> "Not selected".toLowerCase();
};
System.out.print(str);

# try-with-resource 语句

Java 7 引入了 try-with-resource 语句,类似于 Python 的 with ... as ...

这个语法主要用于一些使用完成以后需要关闭的资源。对于这种资源,如果不及时关闭,可能会出现文件被占用等问题。

下面的代码用原始方法实现了向文件写入 hello, world 以后再读取该文件内容。

final String filename = "test.txt";

FileWriter fileWriter = new FileWriter(filename);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write("hello world!");
bufferedWriter.close();
fileWriter.close();

FileReader fileReader = new FileReader(filename);
BufferedReader bufferedReader = new BufferedReader(fileReader);
System.out.println(bufferedReader.readLine());
bufferedReader.close();
fileReader.close();

如果不关闭 writer,那么 reader 什么都读不到。

这样的写法导致每次使用资源以后都需要手动关闭,不仅是麻烦,而且很容易出现忘记关闭的情况。

try-with-resource 就是解决这个问题,它将资源的使用放在一个语句块 { } 中,语句块结束以后就会自动调用资源的 close 方法。

try (FileWriter fileWriter = new FileWriter(filename);
     BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) {
    bufferedWriter.write("hello world!");
}

try (FileReader fileReader = new FileReader(filename);
     BufferedReader bufferedReader = new BufferedReader(fileReader)) {
    System.out.println(bufferedReader.readLine());
}

这种写法非常方便,而让一个类支持这种语法也非常方便:让这个类实现 AutoClosable 接口的 close 方法即可:

public class Analyzer implements AutoCloseable {
    private final FileReader inputFileReader;
    private final FileWriter outputFileWriter, errorFileWriter;

    protected BufferedReader inputBufferedReader;
    protected BufferedWriter outputBufferedWriter, errorBufferedWriter;

    public Analyzer(String inputFilename,
                    String outputFilename,
                    String errorFilename) throws IOException {
        inputFileReader = new FileReader(inputFilename);
        inputBufferedReader = new BufferedReader(inputFileReader);

        outputFileWriter = new FileWriter(outputFilename);
        outputBufferedWriter = new BufferedWriter(outputFileWriter);

        errorFileWriter = new FileWriter(errorFilename);
        errorBufferedWriter = new BufferedWriter(errorFileWriter);
    }

    @Override
    public void close() throws IOException {
        inputBufferedReader.close();
        outputBufferedWriter.close();
        errorBufferedWriter.close();
        inputFileReader.close();
        outputFileWriter.close();
        errorFileWriter.close();
    }
}

# 方法的可变参数

public static void setNames(String... names) {
    // 此时 names 是 String[]
    for (String name: names)
        System.out.print(name);
}

public static void main(String[] args) {
    printNames("Alice", "Bob");
    // 输出 AliceBob
}

# OOP 面向对象

# extends

继承使用 extends 作为关键字:

class Student extends Person {

}

Java 只允许一个 class 继承自一个类,所以没有多继承。但是 Java 也提供了接口。

# super

超类使用 super 作为关键字。子类引用父类的字段时,name this.namesuper.name 是等价的。


super 还会用于子类方法中调用父类方法的时候。

在子类的构造方法中,super() 表示调用父类的构造方法,且这句话必须出现在子类构造方法的第一行(不过,如果只是调用父类的默认构造方法,这行可以省略),否则会报错:

class Student extends Person {
    protected int score;

    // java: 对super的调用必须是构造器中的第一个语句
    public Student(String name, int age, int score) {
        this.score = score;
        super(name, age);
    }
}

在后面覆写部分,还会有子类方法调用父类方法的情况,调用方式也是 super.methodName();

# final

final 可以用于定义类、方法、或变量。定义后的类不能被继承,定义后的方法不能被覆写。而定义后的变量在初始化后不能被修改(类似于 const)。

final class Person {
}

// java: 无法从最终Person进行继承
class Student extends Person {
}

# static

静态字段和静态方法都是属于类的,所有示例共用一套。

虽然可以使用 实例变量.静态字段 访问,但是还是推荐使用 类名.静态字段 访问静态字段以及静态方法。

静态方法不能访问实例的变量。

静态方法经常用于工具类。例如:Arrays.sort() Math.random()

# 向上转型和向下转型

其实就是子类的实例可以赋给父类的引用。

向下转型失败会报 ClassCastException

Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

可以提前使用 instanceof 判断实例的类型:

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false

Java 14 还提供了简写,在 instanceof 后自动完成了转换:

Object obj = "hello";
if (obj instanceof String s) {
    // 可以直接使用变量s:
    System.out.println(s.toUpperCase());
}

# Override

Java 中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。如果签名不同,就是普通的函数重载(Overload)。

可以用 @Override 显式地声明覆写方法,IDE 会对父子类方法的签名进行比对;也可以不写。

Java 对多态的支持也是动态的:对一个引用类型调用某方法,是调用引用实例的实际对象的方法,而不是这个引用的类的方法。

Person p = new Student();
p.run();
// 实际调用的是 Student 的 run

# Object methods

所有的 class 最终都继承自 Object。Object 定义了几个重要的方法:

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

在需要的时候,可以覆写 Object 的这几个方法。

# abstract

abstract methods(抽象方法),和 C++ 的 virtual functions(虚函数)是一样的,都是没有定义的函数,只是为子类提供一个签名。

包含抽象方法的类叫做抽象类。抽象类不能被实例化。只有在抽象类的子类中把所有抽象方法实现后,才能实例化这个子类。

# interface & implements & extends

# 用 interface 定义接口

如果一个抽象类没有成员,并且所有方法都是抽象方法,这个抽象类就可以被改写为接口 interface

// 改写前的抽象类
abstract class Person {
    public abstract void run();
    public abstract String getName();
}

// 改写后的抽象方法
interface Person {
    void run();
    String getName();
}

# 具体的类使用 implements 实现接口

需要注意的是,子类去继承抽象类,使用的是 extends;而一个具体的类实现接口的时候,使用的是 implements

class Student implements Person {
    //...

Java 不允许多继承,但允许实现多个接口:

class Student implements Person, Hello {
    // 实现了两个interface
    // ...
}

# 子接口使用 extends 扩展父接口

而接口又是允许使用 extends 关键字来继承的。

interface Hello {
    void hello();
}

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

# 接口也可以用 default 定义非抽象方法

实现类可以不必覆写 default 方法。default 方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是 default 方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

default 方法和抽象类的普通方法是有所不同的。因为 interface 没有字段,故 default 方法无法访问字段,而抽象类的普通方法可以访问实例字段。

# 接口也可以有 public static final 字段

接口不能有实例字段,但是可以有 public static final 字段,并且可以省略。

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

# package

package 其实就是 Java 的名称空间。

一个文件就是一个包。用 package 声明一个包。import 引入一个包类。

包可以使用域名倒置的形式命名,同时文件的存放位置最好也要和包的顺序相同。

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

# class 套娃

class 套娃的用法会有一点不一样。内部类一定要依附于一个外部类实例。

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer("Nested"); // 实例化一个Outer
        Outer.Inner inner = outer.new Inner(); // 实例化一个Inner
        inner.hello();
    }
}

class Outer {
    private String name;

    Outer(String name) {
        this.name = name;
    }

    class Inner {
        void hello() {
            System.out.println("Hello, " + Outer.this.name);
        }
    }
}

# JavaBean 风格的类

如果读写方法符合以下这种命名规范:

// 读其他字段方法:
public Type getXyz()
// 读 boolean 方法:
public boolean isChild()
// 写方法:
public void setXyz(Type value)

那么这种 class 被称为 JavaBean

# Enum 枚举类

使用普通类的实现方法:

public class Weekday {
    public static final int SUN = 0;
    public static final int MON = 1;
    public static final int TUE = 2;
    public static final int WED = 3;
    public static final int THU = 4;
    public static final int FRI = 5;
    public static final int SAT = 6;
}

if (day == Weekday.SAT || day == Weekday.SUN) {
    // TODO: work at home
}

也可以使用 enum 定义枚举类:

public class Main {
    public static void main(String[] args) {
        Weekday day = Weekday.SUN;
        if (day == Weekday.SAT || day == Weekday.SUN) {
            System.out.println("Work at home!");
        } else {
            System.out.println("Work at office!");
        }
    }
}

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

enum 定义的类型继承于 java.lang.Enum,该类提供了一些方法:

// name() 返回常量名
String s = Weekday.SUN.name(); // "SUN"
// ordinal() 返回定义的常量的顺序,从0开始计数
int n = Weekday.MON.ordinal(); // 1 

# Record 记录类 (Java 14)

Java 14 引入的 Record 能够方便的定义每个字段为 finalfinal class

public record Point(int x, int y) {}

等价于:

public 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() {
        ...
    }
}

# Exception 异常

捕获异常:

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

public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
    ...
}

调用方也可以不捕获异常,而是在调用方所在的方法定义处也写一个 throws Xxx,就可以通过编译了。

也就是说,如果不想在 Main 里面捕获任何异常的话,可以给 Main 函数定义 throws Exception

public static void main(String[] args) throws Exception {
}

Exception 提供了 e.printStackTrace() 函数(准确的说是 Throwable 提供的),打印异常调用栈。

# 异常屏蔽

如果在执行 catchfinally 语句时都抛出了异常,会导致 catch 语句的异常不能被抛出。

public class Main {
    public static void main(String[] args) {
        try {
            Integer.parseInt("abc");
        } catch (Exception e) {
            System.out.println("catched");
            throw new RuntimeException(e);
        } finally {
            System.out.println("finally");
            throw new IllegalArgumentException();
        }
    }
}

结果如下:

catched
finally
Exception in thread "main" java.lang.IllegalArgumentException
	at Main.main(Main.java:10)

也有解决办法,但 IDEA 不推荐在 finally 下抛出错误,就略过了。

# Reflection 反射

JVM 加载 class 的原理是:在运行的时候,Java 会为每一个类创建一个 Class 类型的实例,包含了类的很多信息:

┌───────────────────────────┐
│      Class Instance       │──────> String
├───────────────────────────┤
│name = "java.lang.String"  │
├───────────────────────────┤
│package = "java.lang"      │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,...   │
├───────────────────────────┤
│method = indexOf()...      │
└───────────────────────────┘

Class 类就是用于存储类的信息的类。所以,我们可以在 JVM 中通过某个类对应的 Class 读取到了这个类的所有信息,这个过程就是反射

# 获取 Class

三种方法:

// 类.class
Class cls = String.class;

// 变量.getClass(),返回的是变量的类
Number n = (double) 0;
Class cls = n.getClass(); // class java.lang.Double

// Class.forname()
Class cls = Class.forName("java.lang.String");

可以用 == 精确判断类型:

System.out.println(cls == Number.class);    // false
System.out.println(cls == Double.class);    // true

匹配子类应当使用 instanceof

# 创建新的实例 Class.newInstance()

可以使用 Class.newInstance() 创建类的实例,但是只能调用 public 的无参数构造方法。

String t = String.class.newInstance();  // 等价于 new String()
Double d = Double.class.newInstance();  // 报错,Double 没有无参构造方法

# 获取字段 Class.getField() getDelearedField()

  • Field Class.getField(name):只能获取 public 字段,但可以获取父类的字段
  • Field Class.getDeclaredField(name):可以获取 private 字段,但不能获取父类的字段

获取字段的信息:

  • String Field.getName():字段名称
  • Class Field.getType():字段类型
  • int Field.getModifier():字段的修饰符,可以使用 Modifier.isFinal(m) 来获取字段的信息

通过字段获取、设置实例的值:

  • Object Field.get(Object instance)
  • void Field.set(Object instance, Object value)

如果是对 private 字段进行操作,还需要设置允许访问:

  • Field.setAccessible(true);

如果 JVM 运行期存在 SecurityManager,它会根据规则进行检查,可能会阻止 setAccessible(true)。例如,某个 SecurityManager 可能不允许对 javajavax 开头的 package 的类调用 setAccessible(true),这样可以保证 JVM 核心库的安全。

# 获取方法 Class.getMethod() getDeclaredMethod()

  • Method Class.getMethod(name):只能获取 public 方法,但可以获取父类的方法
  • Method Class.getDeclaredMethod(name):可以获取 private 方法,但不能获取父类的方法

获取方法的信息:

  • String Method.getName():方法名称
  • Class Method.getReturnType():方法返回值类型
  • Class[] Method.getParameterType():方法参数类型
  • int Method.getModifier():字段的修饰符,可以使用 Modifier.isFinal(m) 来获取字段的信息

调用方法:

  • Object Method.invoke(Object obj, Object... args)

调用方法仍遵循多态原则。
如果是静态方法,调用时令 obj = null 即可。

如果是调用 private 方法,还需要设置允许访问:

  • Method.setAccessible(true);

# 获取构造方法 Class.getConstructor

  • Constructor Class.getConstructor(Class... parameterTypes)

调用构造方法:

  • Object Constructor.newInstance(Object... initargs)

调用 private 构造方法仍需要:

  • Field.setAccessible(true);

# 获取继承关系

  • Class Class.getSuperClass():获取父类
  • Class[] Class.getInterfaces():获取实现的接口

实例判断类型可以使用 instanceof,类的转换关系可以使用:

  • Class.isAssignableFrom(Class)

# 动态代理

interface 是不能直接实例化的。要想将其实例化,就要编写一个类实现它的方法,这称为静态代理,也是常见的实现方法。

动态代理就是使用 JDK 提供的 Proxy.newProxyInstance() 接口,就可以在在运行时创建一个接口对象(运行期间的变化都被称为动态)。

动态代理不知道有什么用,咕了。

# Annotation 注解(咕咕咕)

注解是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”:

@Resource("hello")
public class Hello {
    @Inject
    int n;

    @PostConstruct
    public void hello(@Param String name) {
        System.out.println(name);
    }

    @Override
    public String toString() {
        return "Hello";
    }
}

Java 的注解本身对函数的逻辑没有任何影响。

底层的库定义好注解以后,被用在我们编写的函数上,在编译/运行阶段,底层的库函数在执行的时候可以读取注解的内容,读取到以后会进行处理。如,在构造函数执行完成后,

# 注解的分类

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

  • @Override:让编译器检查该方法是否正确地实现了覆写;
  • @SuppressWarnings:告诉编译器忽略此处代码产生的警告。 这类注解不会被编译进入 .class 文件,它们在编译后就被编译器扔掉了。

第二类是由工具处理 .class 文件使用的注解,比如有些工具会在加载 class 的时候,对 class 做动态修改,实现一些特殊的功能。这类注解会被编译进入 .class 文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。

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

# Generic 泛型

Generic 即 C++ 的模板 Template。

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

一次编写,就能匹配任意类型 ArrayList<String>ArrayList<Integer> 等等,又通过编译器保证了类型安全:这就是泛型。

另外,不定义泛型的类型时,默认 T 为 Object:

// 编译器警告: Raw use of parameterized class 'ArrayList' 
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

# 泛型的实现:擦拭法

Java语言的泛型实现方式是擦拭法(Type Erasure),擦拭法的含义是,编译器负责检查所有的语法检查,而编译的结果中,所有 T 都被替换为了 Object

擦拭法决定了泛型 <T>

  • 不能是基本类型,例如:int
  • 不能获取带泛型类型的 Class,例如:Pair<String>.class == Pair.class
  • 不能判断带泛型类型的类型,例如:x instanceof Pair<String>
  • 不能实例化T类型,例如:new T()

泛型方法要防止重复定义方法,例如:public boolean equals(T obj)

# 泛型的向上转换、extends 和 super 通配符

泛型的向上转换,只允许 ArrayList<T> 向上转换为 List<T>,不允许 T 发生向上转换,即不允许 ArrayList<Integer> 向上转换为 ArrayList<Number>


但是,如果确实有这样的需求,比如我们想让传入一个 ArrayList<Integer> 执行下面的函数。

static int add(Pair<Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}

向上转换规则说是不能这样转换的,于是又发明了新的语法:

static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}

Pair<? extends Number> 是一个通配符,能够匹配所有 Number 的子类 T 形成的 Pair<T>


反过来,我们也希望存在一个通配符,能够匹配所有 Number 的父类 T 形成的 Pair<T>。它就是 Pair<? super Number>

void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

# Lambda

Lambda 表达式为 (s1, s2) -> {return s1 > s2;}

单方法接口被称为 FunctionalInterface。需要 FunctionalInterface 作为参数的时候,可以传一个 Lambda 表达式进去。

也可以传入静态方法和实例方法,只要参数和返回类型相同(即:方法签名)就行。

# Map Reduce

使用Stream - 廖雪峰的官方网站 (opens new window)

使用 Stream 可以方便地对大数据进行各类处理,还可以利用多线程加速。

# Maven

Maven 基础 - 廖雪峰 (opens new window)

# XML、JSON 的解析

XML 与 JSON (opens new window)

# JSON

Maven:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.3</version>
</dependency>

解析 JSON 为 Book 类:

InputStream input = Main.class.getResourceAsStream("/book.json");
ObjectMapper mapper = new ObjectMapper();
// 反序列化时忽略不存在的JavaBean属性:
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Book book = mapper.readValue(input, Book.class);

将 Book 类序列化:

String json = mapper.writeValueAsString(book);

# JSON 解析日期

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.12.3</version>
</dependency>
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());

# 自定义解析

需要把 BigInteger978-7-111-54742-6 形式的 ISBN 互化。

定义一个 ISBN 反序列化器:

public class IsbnDeserializer extends JsonDeserializer<BigInteger> {
    public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // 读取原始的JSON字符串内容:
        String s = p.getValueAsString();
        if (s != null) {
            try {
                return new BigInteger(s.replace("-", ""));
            } catch (NumberFormatException e) {
                throw new JsonParseException(p, s, e);
            }
        }
        return null;
    }
}

然后,在 Book 类中使用注解标注:

public class Book {
    public String name;
    // 表示反序列化isbn时使用自定义的IsbnDeserializer:
    @JsonDeserialize(using = IsbnDeserializer.class)
    public BigInteger isbn;
}

类似的,自定义序列化时我们需要自定义一个 IsbnSerializer,然后在 Book 类中标注 @JsonSerialize(using = ...) 即可。

# MVC

                   ┌───────────────────────┐
             ┌────>│Controller: UserServlet│
             │     └───────────────────────┘
             │                 │
┌───────┐    │           ┌─────┴─────┐
│Browser│────┘           │Model: User│
│       │<───┐           └─────┬─────┘
└───────┘    │                 │
             │                 ▼
             │     ┌───────────────────────┐
             └─────│    View: user.jsp     │
                   └───────────────────────┘

MVC 即模型层、控制层、视图层分离。

  • 控制层:实现业务逻辑(后端)
  • 模型层:控制层传给视图层的模型和数据(数据)
  • 视图层:把数据展示给用户(前端)