# 前言
虽然说编程语言已经有很多教程,不需要重复造轮子,但是有的时候也不需要看长篇大论的内容,只需要快速了解语言的基本使用方法就行。所以之后再总结的时候还是采取简单的提纲形式进行吧。
# Java 零级
# 基本特性
- 命名:驼峰命名,类名首字母大写
- Java 基本单位由 class 组成。一个 java 文件只能有一个 public class,且需要和 java 文件名一致
- 类的 main 函数签名为 public static void main (String [] args)
- 使用分号作为结尾,区分大小写,区分单双引号
- 类内局部变量优先级高于 this
- boolean 值为 false 和 true
# 输入输出
- 不换行输出:System.out.print ()
- 换行输出:System.out.println ()
- 格式化输出:System.out.printf ()
- 处理输入时,首先 import java.util.Scanner;,然后 Scanner scanner = new Scanner (System.in),然后使用 scanner 的 nextInt、nextLine 等方法来从控制台读取输入
# 数据类型
- 基本类型:byte 1 字节,short 2 字节,char 2 字节,int 4 字节,float 4 字节,boolean 4 字节,long 8 字节,double 8 字节。没有无符号类型
- float 结尾需要加 f,long 结尾需要加 l
- 字符串类型:String
- 空值使用 null
- 数组
- 定义举例:int [] a = new int [10]、int [] a = {1,2,3};
- 数组一些方法:数组名.length 用于获取数组大小
- 对数组排序:Arrays.sort (arr) 无返回值,原地排序,增序排列;可选参数是 cmp 函数、startindex 和 endindex
- 数组转字符串:Arrays.toString (arr) 返回 String
# 关键字
- public 等定义公私性放最前,int 等定义类型放最后,其他属性放中间
- final 表示不可变、类不可继承、方法不可被覆写
- var 用于自动类型推断,相当于 c++ 的 auto
- public 表示公有,一个 java 文件只能有一个 public class,且类名需要和文件名相同
- private 表示私有,对外不可使用,只能类内使用,不继承。可以开放公有 setter/getter 方法来处理私有变量
- protect 表示保护,对外不可使用,可以被类继承
- static 表示类的共有变量,区别于每个实例自有的空间,属于类而不属于实例
- extends 表示某个类继承某个类、某个接口继承某个接口
- implements 表示某个类实现某个接口
- throws 表示某个方法会抛出某种类型的异常
- throw 表示抛出异常
- instanceof 检查某个对象是否属于某个类的实例
- abstract 表示某个方法是抽象方法、某个类是抽象类
- default 表示接口的某个方法有默认实现
- package 表示包
- import 用于引入包
- enum 用于定义枚举类
# 面向对象
# 类与方法
- 实例化一个对象:ClassName instanceName = new ClassName ();
- 调用对象的方法:instanceName.function ();
- 使用… 可以设定可变参数,比如 func (String… names); 在调用时可以 instance.func (“1”), instance.func (“1”,“2”),内部相当于数组。但好处是调用的时候可以不需要调用方显式构造出一个数组,并且可变参数当不传入时,视为大小为 0 的数组,而不是 null
- 构造方法:public 类名 (){ …},在 new 的时候自动被调用。可以定义多个构造方法实现重载
- 类内成员自动初始化:引用类型为 null,数字类型为 0,布尔值为 false
- 类内成员定义时也可以赋初始值,构造方法传参会覆盖初始值
- 构造方法可以调用其他构造方法,使用 this (args)
# 类的继承
- 使用 extends 关键字来进行类的继承
- 除了 Object 类,所有的类都有父类,没有显式定义则继承于 Object 类
- 继承类保留父类除了 private 限定以外的字段
- 任何类的构造方法的第一行语句必须调用父类的构造方法。编译器会自动加 super (),但是对于没有默认方法的父类而言,则需要手动调用有参数的 super
- 子类不继承父类的构造方法
- 使用 final 修饰 class,表示该类不可继承
- 从 Java 15 起,可以使用 sealed 修饰 class,并通过 permits 写出能够继承出的子类名(启用添加–enable-preview --source 15),用于规定某个 class 能继承出哪些类
- Java 只允许单继承,即只能继承一个类
- 向上转型:upcasting,即将子类对象赋值给父类,是安全的
- 向下转型:downcasting,将父类强制转换为子类,是不安全的
# 多态
- Override:子类中使用和父类方法签名完全相同的函数,称为覆写
- Overload:类中定义多个方法签名不同的函数,称为重载。注意当方法名、参数相同,仅返回值不同时编译报错
- 多态:对于某个类型的方法调用,实际执行的方法取决于运行时期的实际类型,即引用指向的类型
# 抽象类
- 如果父类方法本身不需要实现功能,而需要子类实现,可以使用 abstract 将它定义为抽象方法
- 包含抽象方法的类是抽象类,还需要在 class 关键字前加 abstract
# 接口
- 如果一个抽象类没有字段,所有方法全部都是抽象方法,可以使用 interface 关键字定义接口
- 接口内的方法不需要定义
- interface 内部不能定义实例字段,而可以定义 public static final 的静态字段,所以必须初始化。public static final 可以省略不写
- 接口内所有方法都是 public abstract,可以省略不写
- 使用 class 实现接口时用 implements 关键字:class ClassName implements InterfaceName
- 一个类不能继承多个类,但是可以实现多个接口
- 接口也可以使用 extends 来继承
- 接口内的方法可以使用 default 关键字来进行实现。使用 default 的方法可以不进行覆写,它不能访问实例字段
# 包
- 位于同一个包的类,可以访问包作用域(不使用 public、protected、private 修饰)的字段与方法
- 编译时,如果是完整类名,则直接使用;否则对类的查找顺序为:当前 package、import 的包、java.lang 包。编译出的 class 文件使用的是完整类名
- 使用 import 来导入包,使用 * 来简写导入
- 如果两个 class 相同,则只能 import 一个,另一个必须使用完整类名
- 包没有父子关系,比如 a.b 和 a.b.c 是不同的两个包
# 内部类、匿名类
- 类 A 嵌套在 B 中,则 A 可以访问 B 的私有成员
- 内部类不能独立存在,需要依赖于外部类实例
- 使用 new 来实例化一个实现某接口的匿名类
- 使用 static 修饰一个内部类,则这个内部类不依赖于外部类的实例,而可以访问外部类的 private 的静态字段和静态方法
# 运算
- 除法和 C 一样,保留整数
- 支持 +=、++ 等,和 C 一样
- 支持三元运算
boolean?case1:case2
- 使用 + 连接字符串 String,和 python、c++ 等一样
- 使用 == 判断引用是否相等,使用 equals 方法判断引用的内容是否相等
# 控制流
- if 的相关控制流和 C 一样
- while、for 内使用 break、continue 时可以结合 tag 来跳转到指定层次
- for each 的用法和 C++ 一样:for (type name : arr)
- switch 和 C 基本一样,不过 case 还可以放字符串
- 从 Java 12 起,switch 可以使用新的方式:
String test = "Test"; | |
// 不需要写 break 了 | |
switch (test) { | |
case "answer1" -> System.out.println("OK"); | |
case "Test" ->{ | |
System.out.println("hi"); | |
} | |
default -> System.out.print("Nah"); | |
} | |
// 新的赋值方式 | |
int a = switch (test) { | |
case "Test" -> 1; | |
default -> { | |
int hash = test.hashCode(); | |
yield hash; // 使用 yield 来作为 switch 的返回值 | |
} | |
} |
# 核心类
# 字符串
- String 是一个特殊的类,可以使用双引号来简单地初始化
- 对字符串进行比较时,使用 equals 方法;忽略大小写时使用 equalsIgnoreCase 方法
- 检查是否包含子串:使用 contains 方法
- 查询子串索引:indexOf、lastIndexOf
- 判断是否以某子串开头或结尾:startsWith、endsWith
- 字符串截取:substring (index)、substring (start,end)
- 移除首尾空白字符:trim
- 判断字符串长度是否为 0:isEmpty
- 判断字符串是否包含非空白符:isBlank
- 字符串替换(原地替换,修改原引用):replace (old, new)
- 使用正则对字符串进行替换(原地替换):replaceAll (pattern, new)
- 使用正则对字符串进行切分,返回 String []:split (pattern)
- 字符串拼接:String.join (String, String [])
- 格式化字符串:String.format (string, arg…)、formatted (stirng, arg…)
- 将其他类型的变量转为字符串:String.valueOf (arg)
- 将字符串转为其他类型的变量:Integer.parseInt (string, [radix])、Boolean.parseBoolean (string) 等等
- 字符串与 char [] 的相互转换:str.toCharArray () 得到 char 数组、new String (char []) 得到 String
- 字符串与 bytes [] 的相互转换:str.getBytes (StandardCharsets.UTF_8)、str.getBytes (“UTF-8”)、new String (bytes [], StandardCharsets.UTF_8) 也就是说要注意编码格式,一般采取 utf8
- 使用 + 可以连接字符串,但是大量连接时造成临时空间的浪费。采用 StringBuilder 来预分配缓冲区,新增字符时不会创建新的临时对象。使用 append () 方法向 StringBuilder 对象添加字符串(不需要接收返回值),使用 toString () 方法获取 String。append 方法返回 this,可以链式调用
- StringBuffer 功能和 StringBuilder 一样,不过它实现了多线程安全,但开销大,一般不使用
# 包装类
- Java 的基本类型 byte、short、int、long、boolean、float、double、char 都有对应的引用类:Byte、Short、Integer、Long、Float、Double、Character
- 包装类和对应的基本类型可以相互转换:Integer n = Integer.valueOf (i)、int i = n.intValue ()
- 自动装箱(auto boxing)与自动拆箱(auto unboxing):使用 Integer n = i、int i = n 可以由 Java 编译器在编译阶段自动处理类型
- 所有的包装类型都是不变类,即内部的 value 是 final 的。一旦创建该对象,即不可变。对于包装类型的实例进行比较时要用 equals ()
- Java 的包装类型里有一些有意思的静态变量,比如 Boolean.TRUE、Integer.MAX_VALUE、Long.SIZE、Long.BYTES 等
- 整数和浮点数的包装类继承于 Number,可以用 upcasting 获取各种基本类型
- Java 没有无符号整型,可用包装类的静态方法 toUnsignedInt 来实现
# JavaBean
Java 中许多类有这样的特点:有一些 private 实例字段,通过 public 方法来对其进行读写。如果读写方法(getter/setter)命名满足 getXxx、setXxx、isXxx 等,则这样的类可以称为 JavaBean。JavaBean 用于将数据组合起来,传递数据。JavaBean 可以被 IDE 自动分析快速生成 getter/setter。可以使用 Java 核心库提供的 Introspector 来枚举 JavaBean 的属性:
public class Main { | |
public static void main(String[] args) throws Exception { | |
BeanInfo info = Introspector.getBeanInfo(Person.class); | |
for (PropertyDescriptor pd : info.getPropertyDescriptors()) { | |
System.out.println(pd.getName()); | |
System.out.println(" " + pd.getReadMethod()); | |
System.out.println(" " + pd.getWriteMethod()); | |
} | |
} | |
} | |
class Person { | |
private String name; | |
private int age; | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public int getAge() { | |
return age; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
} |
输出结果为
age | |
public int Person.getAge() | |
public void Person.setAge(int) | |
class | |
public final native java.lang.Class java.lang.Object.getClass() | |
null | |
name | |
public java.lang.String Person.getName() | |
public void Person.setName(java.lang.String) |
# 枚举类
- 使用 enum 可以定义枚举类,让编译器自动检查某个值在枚举的集合内,可以分开不同用途的枚举
- enum 内只需要依次列出变量名即可
- enum 定义的枚举类是引用类型,不过由于每个常量在 JVM 只有一个实例,所以可以直接用 ==(一般情况下用 equals 判断相等)
- enum 定义的类和 class 没有什么区别,不过有以下特点:
- enum 继承于 java.lang.Enum,且不能再被继承
- 只能定义出 enum 的实例,而且不能用 new 来创建实例
- 每个实例都是引用类型的唯一实例
- enum 可以用于 switch 语句
- 可以使用 name () 方法获得枚举名称,使用 ordinal () 获取定义顺序
- 可以为 enum 编写构造方法、字段、方法,构造方法要声明为 private,字段要声明为 final
public enum Color { | |
RED, GREEN, BLUE; | |
} | |
// 相当于 | |
public final class Color extends Enum { // 继承自 Enum,标记为 final class | |
// 每个实例均为全局唯一: | |
public static final Color RED = new Color(); | |
public static final Color GREEN = new Color(); | |
public static final Color BLUE = new Color(); | |
//private 构造方法,确保外部无法调用 new 操作符: | |
private Color() {} | |
} |
# 记录类(Java 14)
- 使用 record 关键字将某个类定义为不变类(数据类)
- 编译器会用 final 修饰类和每个字段,并创建构造方法、和字段名同名的方法,并覆写了 toString、equals、hashCode
- Compact Constructor:可以编写检查逻辑
- 可以为记录类添加静态方法,一种常用的方法命名为 of (),用于创建数据类
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() { | |
... | |
} | |
} |
public record Point(int x, int y) { | |
public Point { | |
if (x < 0 || y < 0) { | |
throw new IllegalArgumentException(); | |
} | |
} | |
} | |
// 相当于 | |
public final class Point extends Record { | |
public Point(int x, int y) { | |
// 这是我们编写的 Compact Constructor: | |
if (x < 0 || y < 0) { | |
throw new IllegalArgumentException(); | |
} | |
// 这是编译器继续生成的赋值代码: | |
this.x = x; | |
this.y = y; | |
} | |
... | |
} |
# BigInteger
- java.math.BigInteger 用来表示任意大小的整数,内部用 int [] 模拟很大的整数。对它做运算只能使用实例方法
- 创建 BigInteger 实例:BigInteger x = new BigInteger (string)
- BigInteger 和 Integer、Long 一样,也是不可变类,并继承自 Number 类
- 使用 Number 定义的 byteValue、intValue 等方法可以将 BigInteger 转化为基本类型,可能会丢失高位信息。如果溢出了原本类型,会得到一个 Infinity
- 使用 intValueExact、longValueExact 等方法可以在不丢失高位信息的情况下转化为基本类型,但如果溢出,会抛出 ArithmeticException 异常
# BigDecimal
- java.math.BigDecimal 可以表示任意大小、精度完全准确的浮点数,它是继承自 Number 的不可变类
- 使用 scale 方法获取小数位数。如果返回负数,说明这个数是整数、末尾有这么多个 0
- 使用 stripTrailingZeros 方法可以将 BigDecimal 格式化为大小相等、去掉末尾 0 的 BigDecimal
- 使用 setScale 方法设置 scale,同时需要传入精度方法,比如 RoundingMode.HALF_UP 表示四舍五入、RoundingMode.DOWN 表示直接截断
- BigDecimal 加减乘不丢失精度,但是除法因为存在除不尽的情况,必须指定精度以及截断方式
- divideAndRemainder 方法会返回一个包含两个 BigDecimal 的数组,分别表示商和余数,其中商总是整数
- 使用 equals 进行相等判断时,不仅要求值相等,也要求 scale 相等
- 使用 compareTo 来进行比较,返回正数负数 0 表示小于大于等于
# 常用工具类
- Math
进行数学计算的类,提供了大量的静态方法。
- Math.abs () 求绝对值
- Math.max (), Math.min () 求最值
- Math.pow () 求幂运算
- Math.sqrt () 开方运算,返回正数
- Math.exp () 自然对数幂
- Math.log () 自然对数
- Math.log10 () 以 10 为底的对数
- Math.sin/cos/tan/asin/acos () 三角函数、反三角函数
- Math.random () 生成 [0,1) 的随机数
- Math.PI, Math.E 常量 π 和 e
- Random
用于创建伪随机数。使用 Random r = new Random () 创建一个对象,其中可以传入 seed。不传入则默认使用系统时间戳
- r.nextInt/Long/Float/Double () 生成一个随机 int/Long/Float/Double
- r.nextInt (n) 生成一个 [0,n) 的随机整数
- SecureRandom
用于产生真随机数,使用 RNG 算法生成,不能指定种子。它的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过 CPU 的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的 “熵”。用法如下:
import java.util.Arrays; | |
import java.security.SecureRandom; | |
import java.security.NoSuchAlgorithmException; | |
public class Main { | |
public static void main(String[] args) { | |
SecureRandom sr = null; | |
try { | |
//SecureRandom.getInstance ("NativePRNGNonBlocking"); // 非阻塞方法 | |
sr = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器 | |
} catch (NoSuchAlgorithmException e) { | |
sr = new SecureRandom(); // 获取普通的安全随机数生成器 | |
} | |
byte[] buffer = new byte[16]; | |
sr.nextBytes(buffer); // 用安全随机数填充 buffer | |
System.out.println(Arrays.toString(buffer)); | |
} | |
} |
# 异常处理
- 异常分为 Error 和 Exception,都继承自 Throwable
- Error 表示严重错误,比如 OutOfMemoryError 内存用尽、NoClassDefFoundError 无法加载类、StackOverflowError 栈溢出
- Exception 表示运行时错误,比如 NumberFormatException 数值类型错误、SocketException 读取网络失败、NullPointerException 调用 null 对象的方法或字段、IndexOutOfBoundException 数组越界
- Exception 可以分为 RuntimeException 和非 RuntimeException
- Java 规定,Exception 及其子类(除 RuntimeException 及其子类)必须捕获,否则无法通过编译,称为 Checked Exception;Error 及其子类、RuntimeException 及其子类不需要捕获
# 捕获异常
- 使用 try {…} catch (…) {…} 来对异常进行处理,还可以继续追加 finally {…},无论是否发生异常都会执行
- 某些方法定义时会加上 throws xxx 表示该方法会抛出的异常类型,那么调用这些方法时必须捕获这些异常
- catch 到异常对象后,使用 printStackTrace () 方法可以打印异常信息
- catch 可以写多个,要求先捕获子类 Exception
public static void main(String[] args) { | |
try { | |
process1(); | |
process2(); | |
process3(); | |
} catch (IOException | NumberFormatException e) { // IOException 或 NumberFormatException | |
System.out.println("Bad input"); | |
} catch (Exception e) { | |
System.out.println("Unknown error"); | |
} finally { | |
System.out.println("OK!"); | |
} | |
} |
# 抛出异常
- 如果某个方法产生异常而没有被处理,则这个异常会被抛到上层方法,直到遇到 try catch
- 使用 throw new XXX (e) 来抛出异常,其中 e 是当前捕获的异常
- finally 中如果抛出异常,会屏蔽掉 catch 到的异常。但是抛出的异常包含 catch 到的异常,可以用 addSuppressed 方法将 catch 的异常添加到 finally 抛出的异常里,上层用 getSuppressed 来获取
# 自定义异常
使用自定义异常时,一般先从某个异常继承出一个 BaseException,然后在其基础上派生出其他自定义类型的 Exception
# Java 一级
# 反射
反射,即 Reflection,指的是在运行期间获得对象的所有信息。解决的问题是在运行期没有获取到某实例任何信息的情况下调用其方法。JVM 在第一次读取到一种 class 类型时,将它加载进内存。每加载一种 class,就创建唯一的一个 Class 类型的实例(仅 JVM 有权限创建 Class 实例)。每个这种 Class 实例指向一个数据类型(class/interface),包含了该 class 的全部信息,比如类名、父类、实现的接口、所有方法和字段等。
从这种 Class 实例获取 class 信息的方法称之为反射。
# 获取 Class
- 反射方法 1:类名.class
- 反射方法 2:实例变量.getClass ()
- 反射方法 3:Class.forName (完整类名)
- 由于 Class 实例在 JVM 中是唯一的,所以这三种方法获取的都是同一个实例,可以用 == 进行比较
- 注意 instanceof 关键字对子类也返回 true,而 class 精确匹配类
- 获取到 Class 后,可以用 newInstance () 方法创建一个实例,局限是只能调用 public 的无参数构造方法
- JVM 在执行 Java 程序的时候,是只在第一次用到 class 的时候才进行加载,以此实现动态加载
# 访问字段
- Field getField (name):根据字段名获取某个 public 的 field(包括父类)
- Field getDeclaredField (name):根据字段名获取当前类的某个 field(不包括父类)
- Field [] getFields ():获取所有 public 的 field(包括父类)
- Field [] getDeclaredFields ():获取当前类的所有 field(不包括父类)
对于 Field 对象,其中包含了该字段所有的信息
- getName ():返回字段名称,例如,“name”;
- getType ():返回字段类型,也是一个 Class 实例,例如,String.class;
- getModifiers ():返回字段的修饰符,它是一个 int,不同的 bit 表示不同的含义。
public final class String { | |
private final byte[] value; | |
} | |
Field f = String.class.getDeclaredField("value"); | |
f.getName(); // "value" | |
f.getType(); //class [B 表示 byte [] 类型 | |
int m = f.getModifiers(); | |
Modifier.isFinal(m); // true | |
Modifier.isPublic(m); // false | |
Modifier.isProtected(m); // false | |
Modifier.isPrivate(m); // true | |
Modifier.isStatic(m); // false |
从对象中获取、修改字段的值:
public class Main { | |
public static void main(String[] args) throws Exception { | |
Person p = new Person("Xiao Ming"); | |
System.out.println(p.getName()); // "Xiao Ming" | |
Class c = p.getClass(); | |
Field f = c.getDeclaredField("name"); | |
f.setAccessible(true); // 对于私有变量,使用 setAccessible 方法将它视为可以访问的;不过 SecurityManager 可能会阻止 | |
Object value = f.get(p); | |
System.out.println(value); // "Xiao Ming" | |
f.set(p, "Xiao Hong"); | |
System.out.println(p.getName()); // "Xiao Hong" | |
} | |
} | |
class Person { | |
private String name; | |
public Person(String name) { | |
this.name = name; | |
} | |
public String getName() { | |
return this.name; | |
} | |
} |
# 调用方法
通过 Class 实例可以获取所有 Field 对象,也可以通过 Class 实例获取所有 Method 信息。Class 类提供了以下几个方法来获取 Method。其中 name 为方法名,Class… 为这个方法的参数的 Class
- Method getMethod (name, Class…):获取某个 public 的 Method(包括父类)
- Method getDeclaredMethod (name, Class…):获取当前类的某个 Method(不包括父类)
- Method [] getMethods ():获取所有 public 的 Method(包括父类)
- Method [] getDeclaredMethods ():获取当前类的所有 Method(不包括父类)
对于 Method 对象,其中包含了方法的所有信息:
- getName ():返回方法名称
- getReturnType ():返回方法返回值类型,也是一个 Class 实例
- getParameterTypes ():返回方法的参数类型,是一个 Class 数组
- getModifiers ():返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义
获取到 Method 对象后,可以通过它的 invoke 方法来调用。invoke 函数第一个参数是实例对象(如果调用的是静态方法,则填 null),然后之后的参数是方法使用的参数。注意如果存在方法重载时,invoke 参数要与 Method 对象匹配。如果调用非 public 方法,也要通过 setAccessible (true) 来设定为可调用。
如果方法存在覆写,则也遵循多态原则,即调用实际类型的覆写方法(如果存在的话)
# 获取继承关系
- Class getSuperclass ():获取父类 class
- Class [] getInterfaces ():获取实现的接口类型
- classA.isAssignableFrom (classB):判断 classB 能否向上转型为 classA
# 动态代理
接口不能实例化,需要用类来实现。接口变量总是通过某个实现它的实例向上转型并赋值给接口变量的。但是编写比较麻烦,所以使用动态代理的方法可以在运行期间创建某个 interface 的实例。
import java.lang.reflect.InvocationHandler; | |
import java.lang.reflect.Method; | |
import java.lang.reflect.Proxy; | |
public class Main { | |
public static void main(String[] args) { | |
InvocationHandler handler = new InvocationHandler() { | |
@Override | |
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { | |
System.out.println(method); | |
if (method.getName().equals("morning")) { | |
System.out.println("Good morning, " + args[0]); | |
} | |
return null; | |
} | |
}; | |
Hello hello = (Hello) Proxy.newProxyInstance( | |
Hello.class.getClassLoader(), // 传入 ClassLoader | |
new Class[] { Hello.class }, // 传入要实现的接口 | |
handler); // 传入处理调用方法的 InvocationHandler | |
hello.morning("Bob"); | |
} | |
} | |
interface Hello { | |
void morning(String name); | |
} |
# 注解
注解是放在 Java 源码的类、方法、字段、参数前的一种特殊的注释,能被编译器打包进 class 文件中。注解本身不影响代码逻辑,而由工具决定如何使用,可以分为编译器使用、处理.class 时使用、程序运行期间使用。
注解在定义时可以配置参数,包括基本类型、String、枚举、这些类型的数组等。配置参数必须是常量,所以注解在定义时就已经确定了参数的值。可以使用默认值,缺少配置参数时则使用默认值。
大部分注解会有一个名为 value 的配置参数,对这个参数进行赋值可以只写常量,相当于省略了 value 参数。如果只写注解,相当于全部使用默认值。
# 元注解
用于修饰注解的注解称为元注解,通常不需要自己编写实现元注解。
- @Target
最常用的元注解是 @Target
,可以定义注解用于源码的哪些位置,必须设定。
- ElementType.TYPE:用于类或者接口
- ElementType.FIELD:用于字段
- ElementType.METHOD:用于方法
- ElementType.CONSTRUCTOR:用于构造方法
- ElementType.PARAMETER:用于方法参数
- @Retention
用于定义注解的生命周期。
- RetentionPolicy.SOURCE:仅编译期,在编译期间被丢掉
- RetentionPolicy.CLASS:仅 class 文件,保存在 class 文件,但不会加载进 JVM
- RetentionPolicy.RUNTIME:仅运行期,会被加载进 JVM,可以在运行期被程序读取
如果不存在,则默认为 CLASS。通常自定义的注释都是 RUNTIME,所以一般都会加上 @Retention(RetentionPolicy.RUNTIME)
- @Repeatable
用于定义注解是否可重复,应用上并不是特别广泛。修饰后,在某处添加被修饰的注解时,可以添加多个。一般情况下不用写。
- @Inherited
用于定义被修饰的类的子类是否也具有这样的注解。仅针对 @Target(ElementType.TYPE)
类型的注解有效,并且仅针对 class 的继承,对 interface 的继承无效。一般情况下不用写。
# 注解定义
Java 使用 @interface
来定义注解,格式如下:
@Repeatable(Reports.class) | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ | |
ElementType.METHOD, | |
ElementType.FIELD | |
}) | |
public @interface Report { | |
int type() default 0; | |
String level() default "info"; | |
String value() default ""; | |
} |
定义时使用 @interface,然后添加内部参数及默认值。将常用的参数定义为 value。最后用元注解来进行配置。其中 @Target 和 @Retention 必须进行设置,其中 @Retention 一般设置为 RUNTIME,因为自定义注解一般在运行期读取。一般情况下不用写 @Inherited 和 @Repeatable。
# 注解处理
一般情况下需要编写使用的是 RUNTIME 类型的 @Retention,需要自行编写处理逻辑。读取注解时使用反射 API。Java 提供的使用反射 API 判断注解的方法有:
- Class.isAnnotationPresent(Class)
- Field.isAnnotationPresent(Class)
- Method.isAnnotationPresent(Class)
- Constructor.isAnnotationPresent(Class)
读取注解的方法有:
- Class.getAnnotation(Class)
- Field.getAnnotation(Class)
- Method.getAnnotation(Class)
- Constructor.getAnnotation(Class)
其中,方法参数本身可以看做数组,而每个参数可以有多个注解,所以一次性获取方法参数的所有注解返回值是二维数组。
# 泛型
泛型是一种代码模板,可以用一套代码来套用各种类型。通过尖括号指定类型,可以创建出不同的对象。比如:
// 创建 ArrayList<Integer > 类型: | |
ArrayList<Integer> integerList = new ArrayList<>(); // 前面已经有 Integer,后面可以省略不写 | |
// 添加一个 Integer: | |
integerList.add(new Integer(123)); | |
// “向上转型” 为 ArrayList<Number>: | |
ArrayList<Number> numberList = integerList; // ArrayList<Integer > 只能向上转型为 List<Integer>,不能转型为 ArrayList<Number> | |
// 添加一个 Float,因为 Float 也是 Number: | |
numberList.add(new Float(12.34)); | |
// 从 ArrayList<Integer > 获取索引为 1 的元素(即添加的 Float): | |
Integer n = integerList.get(1); // ClassCastException! |
Arrays 的 sort 方法可以对数组进行排序,如果是自定义类,可以实现 Comparable<T>
接口并覆写 compareTo
方法,实现自定义类的排序。
# 类型擦除
Java 实现泛型的方法是类型擦除,在编译器完成,虚拟机不感知泛型。类型擦除后,编译器会将 <T>
视为 Object,然后实现安全的强制转型。所以 T 不能是基本类型,因为他们不属于编译器能持有的 Object。另外,没法获得泛型的 Class,因为获得的都是不带泛型的 class;也不能直接实例化 T 类型,因为擦除后都会成为 new Object。如果一定要实例化 T,需要借助 Class<T> cls
, cls.newInstance
。
注意泛型的方法不能覆写其他的方法,比如不能定义 public boolean equals(T obj)
,需要修改函数名。
# extends 通配符
根据前文提到的泛型继承关系,举例而言, Pair<Integer>
不是 Pair<Number>
的子类。因此 Pair<Number>
作为函数参数的类型时,不能接受 Pair<Integer>
类型的参数。
对此,使用 Pair<? extends Number>
替换 Pair<Number>
,可以使得 Number 及 Number 子类比如 Integer、Double 等形成的 Pair 正确传入到函数中。但是,使用通配符的同时,类型擦除也限定了这个变量在使用时只能按 Number 类型,而不能按子类型。
也可以使用 Pair<T extends Number>
来表示只能用 Number 及其子类型来构造 Pair,而不接受其他的比如 String 类型。
<? extends T>
允许调用读方法 T get()
获取 T 的引用,但不允许调用写方法 set(T)
传入 T 的引用(传入 null 除外)
# super 通配符
和 extends 相反,使用 Pair<? super Integer>
可以使得方法参数接受所有泛型类型为 Integer 或 Integer 父类的 Pair,表示方法内部代码对于参数只能写,不能读。
<? super T>
允许调用写方法 set(T)
传入 T 的引用,但不允许调用读方法 T get()
获取 T 的引用(获取 Object 除外)
# 简单示例
public class Test { | |
static Solution a = new Solution(); | |
public static void main(String[] args) { | |
int[] q = {1,8,6,2,5,4,8,3,7}; | |
System.out.println(Test.a.maxArea(q)); | |
} | |
} | |
class Solution { | |
//11. 盛最多水的容器 | |
public int maxArea(int[] height) { | |
int left = 0, right = height.length - 1; | |
int area; | |
for (area = 0; left < right;) { | |
area = Math.max(area,Math.min(height[left],height[right])*(right-left)); | |
if (height[left] < height[right]) { | |
left++; | |
} else { | |
right--; | |
} | |
} | |
return area; | |
} | |
} |