很多都和 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.name
和 super.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 能够方便的定义每个字段为 final
的 final 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
提供的),打印异常调用栈。
# 异常屏蔽
如果在执行 catch
和 finally
语句时都抛出了异常,会导致 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
可能不允许对 java
和 javax
开头的 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 的解析
# 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());
# 自定义解析
需要把 BigInteger
和 978-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 即模型层、控制层、视图层分离。
- 控制层:实现业务逻辑(后端)
- 模型层:控制层传给视图层的模型和数据(数据)
- 视图层:把数据展示给用户(前端)