基础

[toc]

基本语法

  • 除非确实要处理 UTF-16 代码单元,否则不要在程序中使用 char 类型

  • strictfp 关键字修饰的方法中,所有的指令都必须使用严格的浮点计算

    默认情况下,虚拟机设计者允许对中间计算结果采用扩展的精度(扩展的指数),即浮点运算结果不可预知。

    有时我们可能想要得到完全可预测的浮点计算,即浮点计算的结果在各处理器平台都一样,那么就可以使用 strictfp 关键字。但是由于该关键字会对中间结果进行截断操作,而截断操作需要消耗时间,所以在计算速度上比精确计算要慢。因此,在Java中,最优性能与理想结果之间存在冲突。

    strictfp 关键字可用来修饰类、接口或方法。使用 strictfp 关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。严格的浮点计算表示浮点计算完全依照浮点规范 IEEE-754 来执行。需要注意的是,采用严格浮点计算可能会产生溢出,而默认情况下,不会产生溢出。对大多数程序来说, 浮点溢出不属于大向题。

  • 当用二元运算符连接两个值时,先要进行类型转换。优先级 double > float > long > int

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int a = 1;
    double b = 2.1;
    long c = 1L;
    float d = 3.2F;
    char e = 'a';
    byte f = 10
    double res1 = a + b; // double
    float res2 = c + d; // float
    int res3 = e + f; // int
    long res4 = c + e; // long

    1、两操作数中有一个是 double,另一个会被转换为 double 计算;

    2、否则若其中有一个是 float,另一个会被转换为 float 计算;

    3、否则若其中有一个是 long,另一个会被转换为 long 计算;

    4、否则两个操作数都将被转为 int 计算。

  • 二元运算符(+=, -= 之类)。如果运算符得到一个值,其类型与左侧操作数类型不同,则会发生强转。

    1
    2
    int x = 1;
    x += 1.5; // 等价于 x = (int)(x + 1.5)
  • 条件与或运算注意 短路 现象。

    **&& **运算符检查第一个表达式是否返回 false ,如果是 false 则直接返回 false,不再执行右侧的语句。

    || 运算符检查第一个表达式是否返回 true ,如果是 true 则直接返回 true ,不再执行右侧的语句。

    1
    2
    3
    4
    // 当 x 为 0 时,不会执行 1 / x 的检查
    if (x != 0 && 1 / x > 1) {
    // TODO
    }
  • Java 的字符串类似于 C++ 中的 char* 指针

    1
    char* s =  "hello";
  • StringBuffer 效率略低于 StringBuilder

    StringBuffer 支持多线程。

    两个类的 API 一样。

  • for 语句的三个部分应该对同一个计数器变量进行初始化、检测和更新。

  • 由于 switch 语句存在 “直通式”(fall through)行为,程序中不应使用 switch 语句

  • switch中本质上只能使用整型,任何类型的比较都要转换成整型。switch中使用 String 作为条件,会由编译器转化为 hashCode() 进行比较。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class switchDemoString {
    public static void main(String[] args) {
    String str = "world";
    switch (str) {
    case "hello":
    System.out.println("hello");
    break;
    case "world":
    System.out.println("world");
    break;
    default:
    break;
    }
    }
    }
    ---->
    public class switchDemoString {
    public switchDemoString() {
    }
    public static void main(String args[]) {
    String str = "world";
    String s;
    switch((s = str).hashCode()) {
    default:
    break;
    case 99162322:
    // 额外使用 equals(),防止哈希碰撞
    if(s.equals("hello"))
    System.out.println("hello");
    break;
    case 113318802:
    if(s.equals("world"))
    System.out.println("world");
    break;
    }
    }
    }

  • switch 的底层为 tableswitch(适用于 case 范围小,查找方式类似数组的 index )和 lookupswitch(适用于 case 范围大,表中的 case 按序排列,查找方式为二分查找)两种表格

  • 带标签的 break,可用于程序跳转。多用于跳出多重循环。标签必须放在希望跳出的最外层循环之前

    1
    2
    3
    4
    5
    6
    7
    8
    labelOne:
    for (...) {
    for (...) {
    if (...) {
    break labelOne;
    }
    }
    }
  • 数组长度不要求为常量

    数组的简明形式初始化为大括号

    1
    2
    3
    int n = 2;
    int a1 = new int[n]; // 创建一个长度为 n 的数组
    int[] a2 = {1, 2};
  • Arrays.sort() 方法使用的是快排

  • Java 10 之后,引入了关键字 var,用于方法中的局部变量,从变量的初始值推导出它们的类型。

    1
    var p1 = new Person("zhangsan");
  • 参数列表中的省略号 … 表示可以接收任意数量的对象

    1
    2
    3
    4
    5
    6
    public static void main(String[] args) {
    // TODO
    }
    public static void main(String... args) {
    // TODO
    }
  • String.intern() 是一个 native 方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:

    ​ 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。

    ​ 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。

    1
    2
    3
    4
    5
    6
    7
    String s1 = "Java";
    String s2 = s1.intern();
    String s3 = new String("Java");
    String s4 = s3.intern();
    System.out.println(s1 == s2); // true
    System.out.println(s3 == s4); // false
    System.out.println(s1 == s4); //true

    final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。如果编译器在运行时才能知道其确切值的话,就无法对其优化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    final String str1 = "str";
    final String str2 = "ing";
    final String str3 = getStr();
    // 下面两个表达式其实是等价的
    String c = "str" + "ing";// 常量池中的对象
    String d = str1 + str2; // 常量池中的对象
    String e = str1 + str3; // 在堆上创建的新的对象
    System.out.println(c == d);// true
    System.out.println(c == e);// false

    public static String getStr() {
    return "ing";
    }

对象与类

  • 若要返回一个可变数据字段的副本,应该使用 clone(),而不是直接返回

    1
    2
    3
    4
    5
    6
    class Employee{
    ...
    public Date getHireDay() {
    return (Date) hireDay.clone();
    }
    }
  • 类设计技巧:

    1. 一定要保证数据私有

    2. 一定要对数据进行初始化

    3. 不要在类中使用过多的基本类型

      1
      2
      3
      4
      5
      private String firstName;
      private String lastName;
      private int age;
      --->
      private Person p1;
    4. 并非所有字段都需要单独的字段访问器和字段更改器

    5. 分解有过多职责的类

    6. 类名和方法名要体现职责

    7. 优先使用不可变的类

  • this 关键字与 super 关键字

    相同:都具有两个含义。

    • this :1、指示隐式参数的引用;2、调用该类的其他构造器。
    • super:1、调用超类的方法;2、调用超类的构造器。

    不同:this 是对当前对象的引用, 而 super 只是一个只是编译器调用超类方法的特殊关键字,并不是一个对象的引用。

    ​ 由于子类的构造器默认会调用超类的无参构造器,因此若超类无无参构造器,且子类的构造器中未显式调用超类的其他构造器,编译器会报错。

  • 虚拟机预先为每个类计算量一个方法表(method table),列出了所有方法的签名(方法名和参数类型)和调用的实际方法。

    调用某个方法的解析过程如下:

    1. 虚拟机获取对象的实际类型的方法表;
    2. 虚拟机查找定义了该方法签名的类,此时虚拟机已经知道应该调用哪个方法;
    3. 虚拟机调用。
  • 在继承链上进行从上至下的强制类型转换,并谎报对象的内容,则在程序运行时,会抛出一个 ClassCastException 异常。

    1
    Manager boss = (Manager) staff[0]; // Error!

    因此要养成习惯:进行强制类型转换前,使用 instanceof 检查类型

    1
    2
    3
    4
    if (staff[0] instanceof Manager) {
    boss = (Manager) staff[0];
    // TODO
    }
  • 装箱过程是调用包装器的 valueOf() 方法,拆箱过程是调用包装器的 xxxValue 方法

  • String 字符串内容相同,则其散列码也相同,因为其重写了 hashCode()方法,哈希值由字符串内容决定。

    StringBuilder 字符串内容相同,散列码不一定相同,因为其未定义 hashCode() 方法,而 Object 类的默认 hashCode() 方法由对象的存储地址得出。

    1
    2
    3
    4
    5
    6
    String s = "hi";
    String t = "hi";
    System.out.println(s.hashCode() == t.hashCode()); // true
    StringBuilder sb1 = new StringBuilder("hi");
    StringBuilder sb2 = new StringBuilder("hi");
    System.out.println(sb1.hashCode() == sb2.hashCode()); // false
  • 一般的类都会重写 toString() 方法以友好的方式打印对象,令人烦恼的是,数组没有重写,而是继承了 Object 类的 toString()

    调用 Arrays.toString() 打印数组,调用 Arrays.deepToString() 打印多维数组。

    1
    2
    3
    4
    5
    int[] a = {1, 2};
    int[][] b = {{1, 2}, {3}};
    System.out.println(a); // [I@7852e922
    System.out.println(Arrays.toString(a)); // [1, 2]
    System.out.println(Arrays.deepToString(b)); // [[1, 2], [3]]
  • 使用 ArrayList 实现运行时动态更改数组大小,ArrayList 类似数组,但在添加或删除元素时,自动调整数组容量。

    1
    2
    ArrayList<Employee> staff = new ArrayList<>();
    staff.add(new Employee("Zhangsan", 18));
  • 枚举类:

    1
    public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE}

    实质上是定义了一个类,刚好有 4 个实例对象,且不可能构造新的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public final class Size extends Enum
    {
    private Size(String s, int i)
    {
    super(s, i);
    }
    // ...
    static
    {
    SMALL = new T("SMALL", 0);
    MEDIUM = new T("MEDIUM", 1);
    LARGE = new T("LARGE", 2);
    EXTRA_LARGE = new T(" EXTRA_LARGE", 3);
    ENUM$VALUES = (new T[] {
    SMALL, MEDIUM, LARGE, EXTRA_LARGE
    });
    }
    }

    因此,在比较两个枚举类型的值时,不需要调用 equals(),直接使用等号 = 即可。

  • 序列化:

    序列化操作的是对象(Object),即实例化后的类(Class),static 变量不属于任何对象,其不会被序列化。

    transient 关键字修饰变量,阻止该变量序列化;当对象被反序列化时,被 transient 修饰的变量将会被置成类型的默认值。

接口、lambda 表达式和内部类

  • 接口与抽象类:

    共同点:

    ​ 都不能被实例化;

    ​ 都可以包含抽象方法;

    ​ 都可以有默认实现的方法(Java8 可以用 default 关键字在接口中定义默认方法)

    不同点:

    ​ 接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。

    ​ 一个类只能继承一个类,但是可以实现多个接口。

    ​ 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

  • 一个类用到其他的类,但是又不能访问这个类的成员,若想要交换此类的两个对象,用封装类(Wrapper Class)解决。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class Car {
    int model, no;
    Car(int model, int no) {
    this.model = model;
    this.no = no;
    }
    void print() {
    System.out.println("no = " + no + ", model = " + model);
    }
    }

    class CarWrapper {
    Car c;

    CarWrapper(Car c) {this.c = c;}
    }

    class Main {
    // 即将待操作的对象作为 wrapper 类的成员变量
    public static void swap(CarWrapper cw1, CarWrapper cw2) {
    Car temp = cw1.c;
    cw1.c = cw2.c;
    cw2.c = temp;
    }

    public static void main(String[] args) {
    Car c1 = new Car(101, 1);
    Car c2 = new Car(202, 2);
    CarWrapper cw1 = new CarWrapper(c1);
    CarWrapper cw2 = new CarWrapper(c2);
    swap(cw1, cw2);
    cw1.c.print();
    cw2.c.print();
    }
    }

  • 解决默认方法冲突的规则

    1. 超类优先。若超类提供了一个具体的方法,接口中的标签相同的默认方法会被忽略。
    1
    2
    3
    4
      class Student extends Person implements Named {
    // TODO
    }
    // Student 从 Person 继承了方法 getName(),则 Named 接口是否提供了 getName() 的默认实现不会有任何区别
    1. 接口冲突。实现的两个接口提供了标签相同的方法,必须覆盖该方法以解决冲突。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    interface Person {
    default String getName() {
    return "";
    }
    }

    interface Named {
    String getName();
    }

    // 编译器要求必须要解决二义性问题,即必须覆盖
    class Student implements Person, Named {
    public String getName() {
    return "ss";
    }
    // TODO
    }
  • 一个 lambda 表达式只在某些分支返回一个值,而在另外一些分支不返回值,是不合法的。

    1
    (int x) -> {if (x > 0) return 1;} // ERROR
  • 只有一个抽象方法的接口称为函数式接口(functional interface),可通过 lambda 表达式创建该接口的对象。

    本质上虚拟机新定义了一个 private static 方法,并 return 该方法的返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @FunctionalInterface
    public interface MyNumber{
    public double getValue();
    }

    public String strHandler(String str, MyFunction mf) {
    return mf.getValue(str);
    }
    String trimStr = strHandler("\t\t hello ", (str) -> str.trim());
    String upperStr = strHandler("abcdefg", (str) -> str.toUpperCase());
    String newStr = strHandler("abcdefghi", (str) -> str.substring(2, 5));

  • lambda 表达式的体与嵌套块有相同的作用域,因此在 lambda 表达式声明一个与局部变量同名的参数或局部变量是不合法的。

  • lambda 表达式中“捕获”的变量必须是事实最终变量(effectively final),即该变量初始化后不会再为它重新赋值。

    1
    2
    3
    4
    5
    for(int i = 0; i < count; ++i) {
    ActionListener listener = event -> {
    System.out.println(i); // ERROR:Cannot refer to changing i
    }
    }
  • 局部类能够访问局部变量,但访问的局部变量必须是事实最终变量 (effectively final),且会被进行保存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public void start(int internal, boolean beep) {
    class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent e) {
    System.out.println(Instant.ofEpochMilli(e.getWhen()));
    if (beep) {
    ToolKit.getDefaultToolkit().beep();
    }
    }
    }
    var listener = new TimePrinter();
    var timer = new Timer(internal, listener);
    timer.start();
    }

    // 编译器为这个局部内部类合成了 TalkingClock$TimePrinter
    class TalkingClock$TimePrinter {
    TalkingClock$TimePrinter(TalkingClock, boolean);
    public void actionPerformed(java.awt.event.ActionEvent);

    final boolean val$beep;
    final TalkingClock this$0;
    }

异常

  • Java 的异常层次

    1
    2
    3
    4
    5
    6
    7
    Throwable
    ├─Error
    │ └─*
    └─Exception
    ├─IOException
    ├─RuntimeException
    └─*

    将派生于 Error 类或 RuntimeException 类的异常称为非检查型(unchecked)异常,其他的异常称为检查型(checked)异常

  • 不应当声明从非检查型异常继承的异常。

  • 超类未抛出检查型异常,子类也不能抛出任何检查型异常。

    超类抛出异常,子类抛出的异常不能比超类的异常更通用,即可以抛出更特定的异常或不抛出异常

  • finally 用于清理资源,而不应该有改变控制流的语句。

  • try with resources 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try (Resource res = ...) {
    // Do with res
    }
    // catch 和 finally 字句非必要
    catch (...) {
    //
    }
    finally {
    // ...
    }

    Resource 继承了 AutoCloseable 接口,实现了 close() 方法,则在 try 语句退出时,会自动调用 res.close()

    若有 catchfinally 字句,则在关闭资源之后执行。

  • 不要在 finally 语句中使用 return。当 try 语句和 finally 语句都有 return 时,try 语句中的 return 会被忽略。

    因为 try 中的 return 会先暂存在一个本地变量中,执行完 finally 再返回,而若 finally 也有 return,这个本地变量的值就会被 finally 中的 return 替换。

  • assert 实际上是抛出一个 AssertionError()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public static void main(String args[]) {
    int a = 1;
    int b = 1;
    assert a == b;
    System.out.println("yes");
    assert a != b : "Noooo";
    System.out.println("no");
    }
    --->
    public static void main(String args[]) {
    int a = 1;
    int b = 1;
    if(!$assertionsDisabled && a != b)
    throw new AssertionError();
    System.out.println("yes");
    if(!$assertionsDisabled && a == b) {
    throw new AssertionError("Noooo");
    } else {
    System.out.println("no");
    return;
    }
    }

    static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();

    }

泛型

  • 泛型类相当于普通类的工厂。常使用 E 表示集合的元素类型,<K, V> 表示键值对,T 或 U 表示任意类型。

  • 定义一个带有泛型参数的方法,将类型变量放在修饰符(public static)的后面,返回类型的前面。

    1
    2
    3
    public static <T> T getMiddle(T... a) {
    return a[a.length / 2];
    }
  • 虚拟机中没有泛型,只有普通的类和方法,编译器进行**类型擦除(type erasure)**,即所有的类型参数都会被替换为它们的限定类型:

    1、若泛型无限定类型,如 <T> ,则替换为 Object

    2、若泛型有限定类型,如 <T extends A & B>,则用第一个限定类型替换,此处为 A

    因此,为提高效率,应将标签(tagging)接口,即无方法的接口放到限定列表末尾,如 Serializable

  • 泛型的限定,多个限定类型用 & 分割,且只有第一个限定类型可以是 class

    1
    2
    3
    4
    // 只有 A 可能是 class,B 和 C 必须是 interface
    public static <T extends A & B & C> T fun(T.. a) {
    // TODO
    }
  • 编写一个泛型方法调用时,为了保持类型安全,编译器会在必要时插入强转指令。

  • 类型擦除与多态会发生冲突,因此编译器会在子类中生成一个桥方法来调用父类中的同名方法以保持多态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    DateInterval interval = new DateInterval(...);
    Pair<LocalDate> pair = interval;
    pair.setSecond(aDate);
    /*
    * 由于类型擦除,虚拟机中的 setSecond(LocalDate) 无法覆盖 setSecond(Object),
    * 因此编译器在 DateInterval 类中生成了如下桥方法
    */
    public void setSecond(Object second) {
    setSecond((LocalDate) second);
    }

    在虚拟机中,由参数类型和返回类型共同指定一个方法。

  • 无论 S 与 T 有什么关系,Pair<S>Pair<T> 都没有任何关系。

  • 泛型通配符的使用规则:PECS,即生产者使用 <? extends T>,消费者使用 <? super T>

  • Class 类是泛型的,例如 String.class 实际上是唯一的 Class<String>类的对象。

  • “ for each ” 循环的原理就是使用了普通的 for 循环和迭代器,可以处理任何实现了 Iterable接口的对象,该接口只包含一个抽象方法:

    1
    2
    3
    public interface Iterable<E> {
    Iterator<E> interator();
    }

集合

  • Java 的迭代器位于两个元素之间,当调用next时,迭代器越过下一个元素,并返回刚刚越过的元素的引用,remove()删除上次调用 next()所返回的元素。

  • 集合有两个基本接口:CollectionMap

  • Java 中的所有链表都是双向链接

  • 动态数组的选用:不需要同步时,用 ArrayList;需要同步时,用vector

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素。

  • 对于 Map,要迭代处理 key 和 value,用 forEach(),提供一个接受键和值的 lambda 表达式

    1
    map.forEach((k, v) -> System.out.println(k, v))
  • Queue 是单端队列,只能从一端插入元素,另一端删除元素,根据因为容量问题而导致操作失败后处理方式的不同可以分为两类方法:一种在操作失败后会抛出异常,另一种则会返回特殊值。

    功能 抛出异常 返回特殊值
    插入队尾 add(E e) offer(E e)
    删除队首 remove() poll()
    查询队首元素 element() peek()
  • Deque 提供有 push()pop() 等其他方法,可用于模拟栈。

  • ArrayDeque 是基于可变长的数组和双指针来实现,不支持存储 NULL 数据,当被用作栈时比 Stack 快,当被用作队列时比 queue 快。

    • ArrayList(JDK8)

    • HashMap 通过 key 的 hashCode() 经过扰动函数处理扩散到高位后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

      之所以用 (n - 1) 是因为 n 规定为 2 的整数次幂,n 的二进制表示最低位一定为 0,与 hash 进行按位与操作后最低位仍为 0,这就导致 i 值只能为偶数,浪费了数组中索引为奇数的空间,同时增加了 hash 冲突发生的概率。

      计算哈希时之所以要右移 16 位,是因为当数组长度 n 较小时,n-1 的二进制数高 16 位全部为 0,只能利用到 h 的低 16 位数据,提高了 hash 冲突发生的可能性。

      1
      2
      3
      4
      static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }

      拉链数组长度大于 8 时,调用 treeifyBin()方法,然后根据数组长度是否大于等于 64 ,执行红黑树转换或扩容操作。

      Java 集合部分源码

      并发

  • ReentrantLock保护代码块,必须在 finally 字句中释放锁,否则若临界区代码抛出异常,别的进程将会被阻塞

    1
    2
    3
    4
    5
    6
    mylock.lock();
    try {
    // TODO
    } finally {
    mylock.unlock();
    }
  • 一个锁对象可以有一个或多个相关联的条件对象,用newCondition()获取一个条件对象。当条件不满足时,调用条件对象的 await()方法,进入这个条件的等待集,直到锁可用,另一线程在同一条件上调用 signalAll()方法,接触等待线程的阻塞,使这些线程在当前线程释放锁后竞争访问对象。

  • 若一个方法声明时有synchronized关键字,对象的锁将保护整个方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public synchronized void fun() {
    // TODO
    }
    等价于
    public void fun() {
    this.intrinsicLock.lock();
    try {
    // TODO
    } finally {
    this.intrinsicLock.unlock();
    }
    }
  • 最好用 java.util.concurrent来处理锁定,而不要用 Locksynchronized,除非需要代码简洁或需要锁提供额外的能力。

  • 如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能会被另一个线程并发更新。编译器会插入适当的代码,使得某一个线程对该变量做修改时,这个修改对读取这个变量的其他线程都可见。

  • ThreadLocal类为各个线程提供一个单独的生成器,让每个线程访问对都是存储在线程内部自己的变量副本。

代理

静态代理

静态代理中,对目标对象的每个方法的增强都是手动完成的,非常不灵活(接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。实际应用场景非常非常少。对于 JVM,静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

实现步骤:

  1. 定义一个接口及其实现类;
  2. 创建一个代理类同样实现这个接口
  3. 将目标对象注入代理类;
  4. 在代理类的对应方法调用目标类中的对应方法。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 1、定义一个接口及其实现类;
interface SmsService {
String send(String message);
}
class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

// 2、创建一个代理类同样实现这个接口
class SmsProxy implements SmsService {

private final SmsService smsService;

// 3、将目标对象注入代理类
public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}

@Override
public String send(String message) {
System.out.println("before method send()");
// 4、在代理类的对应方法调用目标类中的对应方法
smsService.send(message);
System.out.println("after method send()");
return null;
}
}

public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("hello");
}
}

// 输出:
// before method send()
// send message:hello
// after method send()

动态代理

动态代理是在运行时动态生成类字节码。动态代理的实现方式有很多种,比如 JDK 动态代理CGLIB 动态代理等等。

JDK 动态代理机制

主要用到 Proxy 类的 newProxyInstance() 方法,通过该方法创建的代理对象在调用方法时,实际执行 hinvoke()方法。只能代理实现了接口的类。

1
2
3
4
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
throws IllegalArgumentException {
......
}

实现步骤:

  1. 定义一个接口及其实现类;
  2. 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 1、定义一个接口及其实现类;
interface SmsService {
String send(String message);
}
class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

// 2、自定义 InvocationHandler
class MyInvocationHandler implements InvocationHandler {

private final Object target;

public MyInvocationHandler(Object target) {
this.target = target;
}

// 重写 invoke() 方法
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}

class JdkProxyFactory {
// 3、通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 创建代理对象
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new MyInvocationHandler(target));
}
}

public class Main {
public static void main(String[] args) {
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("hello");
}
}
// 输出:
// before method send
// send message:hello
// after method send

CGLIB 动态代理机制

CGLIB(Code Generation Library) 是一个基于 ASM 的字节码生成库,它允许在程序运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。

实现步骤:

  1. 定义一个待代理的类;
  2. 自定义 MethodInterceptor 并重写 intercept() 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  3. 通过 Enhancer 类的 create()创建代理类。

使用前需要添加相关依赖

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1、定义待代理的类
public class AliSmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

// 2、自定义 MethodInterceptor 并重写 intercept() 方法
public class MyMethodInterceptor implements MethodInterceptor {

@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("before method " + method.getName());
// methodProxy.invokeSuper(o, args) 调用原始方法
Object object = methodProxy.invokeSuper(o, args);
System.out.println("after method " + method.getName());
return object;
}
}


public class CglibProxyFactory {
// 3、通过 Enhancer 类的 create() 创建代理类
public static Object getProxy(Class<?> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(clazz.getClassLoader());
enhancer.setSuperclass(clazz);
enhancer.setCallback(new MyMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");

IO

  • IO 模型:

  • BIO(Blocking IO):同步阻塞 IO。应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间

  • NIO(Non-blocking/New IO):IO 多路复用模型。传统的同步非阻塞 IO 模型如下:应用程序会一直发起 read 调用轮询数据是否已经准备好,内核准备数据就绪后,线程阻塞,直到数据从内核空间拷贝到用户空间。IO 多路复用模型对同步非阻塞 IO 模型进行优化:线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用

  • AIO(Asynchronous I/O):NIO 2,异步 IO 模型。基于事件和回调机制实现,程序发起 read 后立即返回,内核处理完成后,操作系统会通知相应的线程进行后续的操作

  • 所有的 IO 流都派生于如下四个抽象基类:

    InputStream/InputReader: 所有字节输入流的基类/所有字符输入流的基类

    OutputStream/OutputWriter: 所有字节输出流的基类/所有字符输出流的基类

  • 一般不会直接单独使用 FileInputStream ,通常会配合 BufferedInputStream来使用(字节缓冲流会先将读取到的字节存放在缓存区,大幅减少系统调用次数,提高读取效率,不是减少磁盘 IO 操作次数(这个 OS 已经帮我们做了)),或者配合其他的 Stream,即设计模式中的装饰器模式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 新建一个 BufferedInputStream 对象
    BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
    // 读取文件的内容并复制到 String 对象中
    String result = new String(bufferedInputStream.readAllBytes());
    System.out.println(result);

    DataInputStream dataInputStream = new DataInputStream(fileInputStream);
    //可以读取任意具体的类型数据
    dataInputStream.readBoolean();
    dataInputStream.readInt();
    dataInputStream.readUTF();

    FileOutputStream 类似,通常配合其他类使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
    BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);

    DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
    // 输出任意数据类型
    dataOutputStream.writeBoolean(true);
    dataOutputStream.writeByte(1);

    ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("output.txt")
    Person person = new Person("zhangsan", 18);
    output.writeObject(person);
  • InputStreamReader 是字节流转换为字符流的桥梁,其子类 FileReader 是基于该基础上的封装,可以直接操作字符文件,即设计模式中的适配器模式

    1
    2
    3
    4
    // 字节流转换为字符流的桥梁
    public class InputStreamReader extends Reader {}
    // 用于读取字符文件
    public class FileReader extends InputStreamReader {}

    OutputStreamWriter 是字符流转换为字节流的桥梁,其子类 FileWriter 是基于该基础上的封装,可以直接将字符写入到文件

    1
    2
    3
    4
    // 字符流转换为字节流的桥梁
    public class OutputStreamWriter extends Writer {}
    // 用于写入字符到文件
    public class FileWriter extends OutputStreamWriter {}
  • RandomAccessFile 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置,常见的一个应用就是实现大文件的断点续传