• Java 学习笔记



    一、集合

    1.1 List

    1.1.1 ArrayList

    ArrayList 内部基于动态数组 Object[] 实现,会根据实际存储的元素数量动态地扩容或缩容。不过 ArrayList 只能存储对象,对于基本数据类型,需要使用其对应的包装类,同时线程不安全

    ArrayList 有三个构造函数,其中无参数构造方法会初始化一个空数组,但不分配实际容量,只有在添加第一个元素时,数组的容量才会扩展为默认大小(通常为 10)。

    ArrayList()
    ArrayList(int initialCapacity)
    ArrayList(Collection<? extends E> c)
    
    • 1
    • 2
    • 3

    ArrayList 在空间不足时会进行动态扩容,扩容时首先会将容量变为原来的 1.5 倍左右(奇数会丢掉小数),然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量。

    1.1.2 Vector

    VectorArrayList 类似也采用 Object[] 实现,是 List 的古老实现类,同时线程安全,但是在增长时会以固定的幅度增加容量,而不是按倍数增加,这可能导致一些内存浪费,因此通常更倾向于使用 ArrayList,并使用显式的同步措施来确保线程安全性。

    1.1.3 LinkedList

    LinkedList 底层使用的是双向链表,不过需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且性能通常会更好。同时 LinkedList不是线程安全的

    1.1.4 CopyOnWriteArrayList

    采用写时复制的线程安全 List。写时复制指的是读取时无需加锁,写入时不直接修改原始数据,而创建一个新的副本,然后在副本上进行修改,最后进行替换。因此 CopyOnWriteArrayList 适用于读多写少的场景。

    CopyOnWriteArrayList 主要有两个问题:

    • 内存占用问题:尽管拷贝操作已经尽可能进行了优化,但是仍然会导致额外的时间开销以及内存占用,因此在数据量比较大或者写操作执行比较频繁的场景下要谨慎考虑。
    • 数据一致性问题CopyOnWriteArrayList 仅能保证数据的最终一致性,在写操作执行完成前的读操作读取到的数据均为旧数组的脏数据。

    1.2 Queue

    1.2.1 ArrayDeque & LinkedList

    ArrayDequeLinkedList 都实现了 Deque 接口,ArrayDeque 基于动态数组实现,并且是循环数组,在队满时会扩容为原来的两倍LinkedList 基于链表实现,速度较慢且存储密度低,因此一般优先考虑 ArrayDeque,不过 ArrayDeque 不支持存储 null 且二者都不是线程安全的。

    ArrayDeque 主要通过 addFirst()addLast()removeFirst()removeLast() 实现双端队列的相关操作,这四个方法失败时都会抛出异常。

    1.2.2 PriorityQueue

    PriorityQueue 本质上是完全二叉树实现的小根堆,能够在 O ( l o g n ) O(logn) O(logn) 的时间复杂度内插入元素和删除堆顶元素。不过 PriorityQueue 线程不安全,且不支持存储 null 。同时,为了进行比较,优先队列存储的元素类型需要实现 Comparable 接口,或者在队列构造时传入自定义比较器 Comparator


    1.3 Map

    1.3.1 HashMap & ConcurrentHashMap

    HashMap 是一个无序的键值对集合,可以存储为 null 的 key 和 value,不过 null 作为键只能有一个,作为值可以有多个。

    创建时如果不指定容量初始值,HashMap 默认的初始化大小为 16,之后每次扩充,容量翻倍。创建时如果给定了初始容量值,HashMap 会将其扩充至大于此初始值的首个 2 n 2^n 2n 大小(主要是为了在减少哈希碰撞同时提高哈希运算的效率,如果数组的长度为 2 n 2^n 2n,那么在映射时只需要直接取键的低 n 位即可,通过位运算即可实现,不需要取余)。

    HashMap 基于数组(桶) + 链表 / 红黑树实现,默认采用数组 + 链表,当链表长度大于 8(如果哈希函数足够优秀,节点在桶中的分布遵循泊松分布,链表长度大于 8 的概率相当低)时会通过 treeifyBin() 处理哈希冲突,如果此时数组长度小于 64 会优先对数组进行扩容,否则会将链表转化为红黑树以减少搜索时间。除了链表长度会导致扩容,当元素个数超过阈值时 HashMap 也会进行扩容,这个阈值默认为当前数组长度的 0.75 (默认的负载因子)倍。

    class Solution {
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
            {
                HashMap<Integer, Integer> map = new HashMap<>();
                Class<? extends HashMap> clazz = map.getClass();
                Field threshold = clazz.getDeclaredField("threshold");
                threshold.setAccessible(true);
                for (int i = 0; i <= 12; i++) {
                    map.put(i, null);
                    System.out.print(threshold.getInt(map) + " ");  // 12 12 12 12 12 12 12 12 12 12 12 12 24
                }
            }
            {
                HashMap<Integer, Integer> map = new HashMap<>();
                Class<? extends HashMap> clazz = map.getClass();
                Field threshold = clazz.getDeclaredField("threshold");
                threshold.setAccessible(true);
                for (int i = 0; i <= 8; i++) {
                    map.put(i * 16, null);
                    System.out.print(threshold.getInt(map) + " ");  // 12 12 12 12 12 12 12 12 24
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    HashMap 是非线程安全的,其并发版本为 ConcurrentHashMapConcurrentHashMap 大多数操作通过自旋 CAS 和 volatile 保证并发安全,只有部分操作通过 synchronized 加锁(比如向非空的桶中添加元素),同时只对单个桶加锁,加锁粒度小,并发效果优秀。不过 ConcurrentHashMap 的 key 和 value 均不能为 null,因为多线程情况下无法准确判断 null 表示的是不存在还是存在一个空的键或值。

    注意ConcurrentHashMap 保证的只是单次操作的原子性,而不是多次操作,因此类似于先通过 containsKey() 判断键是否存在然后再通过 put() 插入元素的操作都是线程不安全的。如果想要执行复合操作,可以通过 putIfAbsent() 以及 compute() 等复合函数代替。

    1.3.2 LinkedHashMap

    LinkedHashMapHashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效

    LinkedHashMap 提供了两种顺序迭代元素的方式:

    • 按照插入顺序迭代元素:按照插入顺序迭代元素是 LinkedHashMap 的默认行为。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。
    • 按照访问顺序迭代元素:可以通过构造函数中的 accessOrder 参数指定按照访问顺序迭代元素。当 accessOrdertrue 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。

    1.3.3 EnumMap

    EnumMap 是 Java 中的一种特殊的 Map 实现,它要求键的类型必须是枚举类型。

    EnumMap 的主要特点:

    • 枚举类型作为键EnumMap 要求键的类型必须是枚举类型,这样可以利用枚举的有限性和固定的值范围。
    • 顺序与枚举常量的定义顺序一致EnumMap 会按照枚举常量的定义顺序来维护键值对的顺序。
    • 高性能:由于 EnumMap 内部使用数组实现,因此在某些情况下,它比通用的 HashMap 更高效。

    1.3.4 TreeMap

    TreeMap 基于红黑树实现,因此元素有序,同时为了保证 key 可比较,key 不允许为 null,线程不安全且性能稍逊于 HashMap


    1.4 Set

    Set 主要包括 HashSetLinkedHashSetTreeSet,本质上是只存储键的 Map,值为一个空的 Object,能保证元素唯一,并且都不是线程安全的


    1.5 注意事项

    • 判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0,因为 isEmpty() 方法的可读性更好并且效率更高,size() == 0 在某些情况下还要进行类型转换。
    • 集合转数组时,使用集合的 collection.toArray(T[] array) 方法,有以下两种使用方式。
      class Solution {
          public static void main(String[] args) {
              List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
              Integer[] array = new Integer[3];
      		
      		// 1. 返回参数
              list.toArray(array);
              for (int i = 0; i < array.length; i++) {
                  System.out.println(i);  // 0 1 2
              }
              
      		// 2. 通过 new Integer[0] 说明返回类型,0 是为了节省空间
              array = list.toArray(new Integer[0]);
              for (int i = 0; i < array.length; i++) {
                  System.out.println(i);  // 0 1 2
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    • 数组转集合时,使用 Arrays.asList(array) 方法。不过该数组必须是对象数组,而不是基本类型数组。同时 Arrays.asList() 方法返回的并不是 java.util.ArrayList,而是 java.util.Arrays 的一个内部类,因此需要一次附加的转换。
      class Solution {
          public static void main(String[] args) {
              Integer[] array = new Integer[] {1, 2, 3};
      
              List<Integer> list = Arrays.asList(array);
              System.out.println(list);  // [1, 2, 3]
              System.out.println(list.getClass());  // class java.util.Arrays$ArrayList
      
              List<Integer> trueList = new ArrayList<>(list);
              trueList.remove(2);
              System.out.println(trueList);  // [1, 2]
              System.out.println(trueList.getClass());  // class java.util.ArrayList
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

    二、异常和错误

    java.lang.Throwable 是所有异常和错误的父类,它有 ExceptionError 两个子类,其中异常又可以分为受检查异常不受检查异常

    • 不受检查异常在编译阶段不会出现错误提醒,主要包括 RuntimeException 及其子类,例如 NullPointerExceptionIllegalArgumentExceptionArrayIndexOutOfBoundsExceptionClassCastException 等异常。
    • 受检查异常在编译阶段就会出现错误提醒,除了 RuntimeException 及其子类,其他的 Exception 类及其子类都属于受检查异常,例如 IOExceptionSQLExceptionClassNotFoundException 等。处理可检查异常的方式可以使用 try-catch-finally 语句块进行捕获和处理,或者在方法签名中声明抛出该异常。
    • Error 类用来表示系统级别的严重错误,例如 OutOfMemoryErrorStackOverflowError 等。对于系统错误,一般不建议进行捕获和处理,而是直接让 JVM 终止。

    Throwable 类常用方法:

    • String getMessage():返回异常发生时的简要描述。
    • String toString():返回异常发生时的详细信息。
    • void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息。

    Exception 支持自定义新的异常类型,但是在项目中保持一个合理的异常继承体系是非常重要的,因此可以定义一个 BaseException 根异常继承自 RuntimeException,其他业务类型的异常再从根异常中派生。

    注意:在通过 try-catch-finally 捕获和处理异常时,不应该在 finally 语句块中使用 return,因为当 try 语句和 finally 语句中都有 return 语句时,try 语句中的返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值,从而导致 try 语句块中的 return 语句会被忽略。同理,catch 中的 return 也会被 finally 中的 return 覆盖。

    class Solution {
        public static void main(String[] args) {
            System.out.println(func());  // 2
        }
    
        private static int func() {
            try {
                return 1;
            } finally {
                return 2;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在 JDK7 之后还提供了 try-with-resources 用于管理文件、网络连接、数据库连接等所有实现了 java.lang.AutoCloseable 接口的资源类,它简化了资源管理的代码,并确保资源在使用后被正确关闭,以避免资源泄漏。


    三、泛型

    泛型是 JDK5 中引入的一个新特性,类似于 C++ 中的模板,泛型编程以一种独立于任何特定类型的方式编写代码。

    Java 中的泛型主要有泛型类泛型接口泛型方法三种使用方式:

    • 泛型类
      public class MyClass<T> {
          private T data;
      
          public T getData() {
              return data;
          }
      
          public void setData(T data) {
              this.data = data;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      class Solution {
          public static void main(String[] args) {
              MyClass<Integer> classInt = new MyClass<>();
              classInt.setData(1);
              System.out.println(classInt.getData());  // 1
      
              MyClass<Double> classDouble = new MyClass<>();
              classDouble.setData(1.0);
              System.out.println(classDouble.getData());  // 1.0
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 泛型接口
      public interface MyInterface<T> {
          T func();
      }
      
      • 1
      • 2
      • 3
      public class MyClass implements MyInterface<String> {
          @Override
          public String func() {
              return "str";
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      class Solution {
          public static void main(String[] args) {
              MyInterface<String> myClass = new MyClass();
              System.out.println(myClass.func());  // str
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 泛型方法
      class Solution {
          public static void main(String[] args) {
              Integer i = 1;
              Long l = 1L;
              
              func(i);  // class java.lang.Integer
              func(l);  // class java.lang.Long
          }
      
          private static <T> void func(T data) {
              System.out.println(data.getClass());
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    泛型擦除:泛型擦除是指 Java 中的泛型只在编译期有效,在运行期间会被删除。也就是说所有泛型参数在编译后都会被清除掉,因此下面两个函数会存在重载异常。

    class Solution {
        public void func(List<String> list) {}
        public void func(List<Integer> list) {}
    }
    
    • 1
    • 2
    • 3
    • 4

    Java 泛型本身不支持协变逆变,但可以通过通配符及 extendsuper 实现类似的效果。

    public static class A {}
    
    public static class B extends A {}
    
    public static class C extends B {}
    
    public static void main(String[] args) {
        B[] array = new C[1];  // 数组支持协变
        List<B> list = new ArrayList<C>();  // 泛型不支持协变
        
        List<? extends B> list;
        list = new ArrayList<A>();  // 编译错误
        list = new ArrayList<B>();
        list = new ArrayList<C>();  // 通过上界模拟协变
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    四、时间和日期

    从 Java8 开始,时间通过 LocalDateLocalTimeLocalDateTime 表示,其中 LocalDate 包含年、月、日信息,LocalTime 包含时、分、秒、和纳秒信息。LocalDateTime 包含了所有信息。

    LocalDateTime 为例介绍相关 API:

    package atreus.ink;
    
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    
    public class Main {
        public static void main(String[] args) {
            // 创建 LocalDateTime 对象
            LocalDateTime dateTime = LocalDateTime.of(2023, 11, 25, 10, 30, 30, 10);
            System.out.println(dateTime);  // 2023-11-25T10:30:30.000000010
            dateTime = LocalDateTime.now();
            System.out.println(dateTime);  // 2023-11-16T10:24:00.149062
    
            // 获取年、月、日、时、分、秒、纳秒
            System.out.println(dateTime.getYear());  // 2023
            System.out.println(dateTime.getMonth());  // NOVEMBER
            System.out.println(dateTime.getMonthValue());  // 11
            System.out.println(dateTime.getDayOfWeek());  // THURSDAY
            System.out.println(dateTime.getDayOfMonth());  // 16
            System.out.println(dateTime.getDayOfYear());  // 320
            System.out.println(dateTime.getHour());  // 10
            System.out.println(dateTime.getMinute());  // 24
            System.out.println(dateTime.getSecond());  // 0
            System.out.println(dateTime.getNano());  // 149062000
    
            // 直接修改(返回新对象)
            System.out.println(dateTime.withYear(2024));  // 2024-11-16T10:24:00.149062
    
            // 增加与减少(返回新对象)
            System.out.println(dateTime.plusYears(1));  // 2024-11-16T10:24:00.149062
            System.out.println(dateTime.minusYears(1));  // 2022-11-16T10:24:00.149062
    
            // 比较
            System.out.println(dateTime.isBefore(dateTime.plusYears(1)));  // true
    
            // 格式化与解析
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss.SSS秒");
            String s = dateTime.format(formatter);
            System.out.println(s);  // 2023年11月16日 10时24分00.149秒
            LocalDateTime time = LocalDateTime.parse(s, formatter);
            System.out.println(time);  // 2024-11-16T10:24:00.149
        }
    }
    
    • 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

    五、Stream 流

    Stream 在 Java8 中被引入,它提供了一种类似于 SQL 语句的方式来对 Java 集合进行操作和处理。

    5.1 获取 Stream 流

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    class Solution {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            String[] array = new String[10];
    		
    		// 获取集合的 Stream 流
            Stream<String> listStream = list.stream();
    		
    		// 获取数组的 Stream 流
            Stream<String> arrayStream = Arrays.stream(array);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5.2 Stream 流的中间方法

    常用中间方法说明
    Stream filter(Predicate predicate)用于对流中的数据进行过滤
    Stream sorted(Comparator comparator)按照指定规则排序
    Stream limit(long maxSize)获取前几个元素
    Stream skip(long n)跳过前几个元素
    Stream distinct()去除流中重复的元素(自定义对象需要重写 equals()hashCode() 方法)
    Stream map(Function mapper)加工元素并返回新流
    Stream concat(stream a, Stream b)合并 a 和 b 为一个流
    Stream boxed();将通过 stream(int[] array) 得到的 IntStream 装箱为 Stream
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    class Solution {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
            
            list.stream().filter(i -> i > 1).forEach(System.out::print);  // 2 3
            list.stream().sorted((o1, o2) -> o2 - o1).forEach(System.out::print);  // 3 2 1
            list.stream().map(i -> i + 1).forEach(System.out::print);  // 2 3 4
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    5.3 Stream 流的终结方法

    常用终结方法说明
    void forEach(Consumer action)遍历
    long count()统计元素个数
    Optional max(Compatator comparator)获取最大值元素
    Optional min(Compatator comparator)获取最小值元素
    R collect(Collector collector)把流中的元素收集到集合
    Object[] toArray()把流中的元素收集到数组

    通过 collect() 实现 List 转 Map:

    public class Main {
        @Data
        @AllArgsConstructor
        private static class MyClass {
            String userName;
        }
    
        public static void main(String[] args) {
            List<MyClass> list = new ArrayList<>();
            list.add(new MyClass("1"));
            list.add(new MyClass("2"));
            list.add(new MyClass("3"));
    
            Map<String, MyClass> map = list.stream().collect(Collectors.toMap(MyClass::getUserName, Function.identity()));
            
            for (Map.Entry<String, MyClass> entry : map.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1: Main.MyClass(userName=1)
    2: Main.MyClass(userName=2)
    3: Main.MyClass(userName=3)
    
    • 1
    • 2
    • 3

    5.4 并行流

    可以通过 parallelStream() 从集合中创建并行流,也可以通过 parallel() 方法将顺序流转换为并行流,并通过 sequential() 方法将并行流转换回顺序流。

    并行流内部默认使用 ForkJoinPool 线程池,默认的线程数量就是处理器的核心数。

    public class Main {
    
        public static void main(String[] args) {
            List list = IntStream.rangeClosed(1, 100000).boxed().toList();
    
            long start = System.nanoTime();
            list.stream().map(integer -> integer + 1);
            long cost = System.nanoTime() - start;
            System.out.println(cost);  // 335875
    
            start = System.nanoTime();
            list.parallelStream().map(integer -> integer + 1);
            cost = System.nanoTime() - start;
            System.out.println(cost);  // 199292
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    六、函数式接口、Lambda 和方法引用

    函数式接口和 Lambda 表达式均为 Java8 新特性。函数式接口就是有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,可以被隐式转换为 Lambda 表达式。Lambda 表达式实际是匿名函数,其原型为:(参数列表) -> {函数体};

    import java.util.*;
    
    class Solution {
        public static void main(String[] args) {
            Integer[] array = new Integer[]{1, 2, 3};
            Arrays.sort(array, new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o2 - o1;
                }
            });
    
            List<Integer> list = Arrays.asList(array);
            list.forEach((i)->{
                System.out.println(i);  // 3 2 1
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    方法引用其实是 Lambda 的语法糖,当我们的 Lambda 表达式只有一行并且恰好有一已经存在的方法作用跟他相同,我们就可以用方法引用替换 Lambda 表达式。方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 表示。

    方法引用主要分为静态方法引用实例方法引用特定类型的方法引用构造器引用

    • 静态方法引用
      public class MyCompare {
          public static int compareFunc(int o1, int o2) {
              return o2 - o1;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      import java.util.*;
      
      class Solution {
          public static void main(String[] args) {
              Integer[] array = new Integer[]{1, 2, 3};
      
              // 静态方法引用
              Arrays.sort(array, MyCompare::compareFunc);
      
              List<Integer> list = Arrays.asList(array);
              list.forEach(System.out::println);  // 3 2 1
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    • 实例方法引用
      public class MyCompare {
          public int compareFunc(int o1, int o2) {
              return o2 - o1;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      import java.util.*;
      
      class Solution {
          public static void main(String[] args) {
              Integer[] array = new Integer[]{1, 2, 3};
              MyCompare compare = new MyCompare();
              
              // 实例方法引用
              Arrays.sort(array, compare::compareFunc);
      
              List<Integer> list = Arrays.asList(array);
              list.forEach(System.out::println);  // 3 2 1
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    • 特定类型的方法引用
      import java.util.*;
      
      class Solution {
          public static void main(String[] args) {
              String[] array = new String[]{"b", "A"};
      
              // 特定类型的方法引用
              Arrays.sort(array, String::compareToIgnoreCase);
      
              List<String> list = Arrays.asList(array);
              list.forEach(System.out::println);  // A b
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    • 构造器引用
      public class MyCompare {}
      
      • 1
      import java.util.function.Supplier;
      
      class Solution {
          public static void main(String[] args) {
              Supplier<MyCompare> sup = MyCompare::new;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    七、反射

    反射机制允许我们在运行时获取类的信息、调用类的方法、操作类的属性,而无需在编译时知道类的具体名称,但存在一定的安全问题,同时会影响性能。

    获取 Class 对象的四种方式:

    package atreus.ink;
    
    public class MyClass {}
    
    • 1
    • 2
    • 3
    import atreus.ink.MyClass;
    
    class Solution {
        public static void main(String[] args) throws ClassNotFoundException {
            {
                // 1. 使用 .class 获取,不会触发类的初始化
                Class<MyClass> clazz = MyClass.class;
                System.out.println(clazz);  // class atreus.ink.MyClass
            }
            {
                // 2. 通过 Class.forName() 传入类的全路径获取
                Class<?> clazz = Class.forName("atreus.ink.MyClass");
                System.out.println(clazz);  // class atreus.ink.MyClass
            }
            {
                // 3. 通过对象实例的 getClass() 方法获取
                MyClass myClass = new MyClass();
                Class<? extends MyClass> clazz = myClass.getClass();
                System.out.println(clazz);  // class atreus.ink.MyClass
            }
            {
                // 4. 通过类加载器的 loadClass() 方法传入类的全路径获取,不会触发类的初始化
                ClassLoader classLoader = MyClass.class.getClassLoader();
                Class<?> clazz = classLoader.loadClass("atreus.ink.MyClass");
                System.out.println(clazz);  // class atreus.ink.MyClass
            }
        }
    }
    
    • 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

    反射的一些基本操作:

    package atreus.ink;
    
    public class MyClass {
        private String data;
    
        public MyClass() {
            data = "atreus";
        }
    
        private void privateMethod() {
            System.out.println("private void privateMethod()");
        }
    
        public String publicMethod(String s) {
            return s;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    class Solution {
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
            // 通过 Class.forName() 传入类的全路径获取 Class 对象
            Class<?> clazz = Class.forName("atreus.ink.MyClass");
    
            // 创建类的实例对象
            Object instance = clazz.getDeclaredConstructor().newInstance();
    
            // 获取类中定义的所有方法
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                System.out.println(method);
            }
    
            // 获取指定方法并调用
            {
                Method method = clazz.getDeclaredMethod("publicMethod", String.class);
                Object result = method.invoke(instance, "public String publicMethod(String s)");
                System.out.println(result);
            }
            {
                Method method = clazz.getDeclaredMethod("privateMethod");
                method.setAccessible(true);
                method.invoke(instance);
            }
    
            // 获取指定参数并在修改后输出
            Field field = clazz.getDeclaredField("data");
            field.setAccessible(true);
            field.set(instance, "new data");
            System.out.println(field.get(instance));
        }
    }
    
    • 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
    public java.lang.String atreus.ink.MyClass.publicMethod(java.lang.String)
    private void atreus.ink.MyClass.privateMethod()
    public String publicMethod(String s)
    private void privateMethod()
    new data
    
    • 1
    • 2
    • 3
    • 4
    • 5

    八、注解

    注解可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供了某些信息供程序在编译或者运行时使用。

    注解本质上是是一个接口,继承自 Annotation 类,注解属性本质则是抽象方法,使用注解实际上使用的是该接口的实现类。可以通过 @interface 自定义注解,且注解中如果只有一个 value 属性,使用注解时 value 名称可以不写

    public @interface MyAnnotation {
        public 属性类型 属性名() default 默认值;
    }
    
    public interface MyAnnotation extends Annotation {
        public abstract 属性类型 属性名();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    public @interface MyAnnotation {
        String value();
    }
    
    • 1
    • 2
    • 3
    class Solution {
        @MyAnnotation("str")  // 可以省略 value
        public static void main(String[] args) {}
    }
    
    • 1
    • 2
    • 3
    • 4

    元注解是修饰注解的注解,主要分为 @Target@Retention

    • @Target:声明被修饰的注解能在哪些位置使用,如类、接口、成员变量、成员方法等。
    • @Retention:声明注解的保留周期。SOURCE 表明只作用在源码阶段,字节码文件中不存在。CLASS 为默认值,表明保留到字节码文件中,但运行阶段不存在。RUNTIME 表明一直保留到运行阶段。

    AnnotatedElement 接口定义了与注解解析相关的方法。注解一般需要与反射结合使用,所有的类成分 Class、Method、Field 和 Constructor 都实现了 AnnotatedElement 接口,它们都拥有解析注解的能力

    主要解析方法有:

    • Annotation[] getDeclaredAnnotations():获得当前对象上使用的所有注解,返回注解数组。
    • T getDeclaredAnnotation(Class annotationClass):根据注解类型获得对应注解对象。
    • boolean isAnnotationPresent(Class annotationClass):判断当前对象是否使用了指定的注解。

    九、引用类型

    Java 中有四种引用类型:

    • 强引用:我们平常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候。
    • 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象会在内存将要溢出(OOM)时被回收。软引用主要用于实现缓存,内存足够时可以避免重复读取,内存不足时可以直接释放。
    • 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,只被弱引用指向的对象就会被回收。弱引用除了用于解决 ThreadLocal 中的内存泄露问题,本地缓存 Caffeine Cache 也支持软引用和弱引用。
    • 虚引用:虚引用是最弱的引用,使用 PhantomReference 进行定义。虚引用的主要用途是跟踪对象的垃圾回收,从而实现直接内存的回收等操作。

    十、Java 运行时数据区域

    Java 运行时数据区域主要分为程序计数器虚拟机栈本地方法栈方法区,其中堆和方法区为线程共享

    10.1 程序计数器

    每个线程会通过自己的程序计数器记录当前要执行的字节码指令的地址

    程序计数器主要有两个作用:

    • 控制指令的执行顺序,实现分支、跳转和异常等逻辑。
    • 在多线程情况下为每个线程记录 CPU 切换前指令的执行位置,便于后续恢复。

    10.2 虚拟机栈

    虚拟机栈的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。它由一个个栈帧组成,调用一个新的函数会在栈上创建一个新的栈帧,当函数返回时这个栈帧会被自动销毁

    如果栈的大小不支持动态扩展,发生栈溢出时就会抛出 StackOverFlowError。如果栈的大小支持动态扩展,在扩展过程中无法申请到足够的内存空间时就会抛出 OutOfMemeoryError,可以通过虚拟机参数 -Xss 修改栈的大小。

    每个栈帧由局部变量表操作数栈帧数据三部分组成:

    • 局部变量表:本质是一个数组,数组中从前向后依次保存了实例方法的 this 对象(非静态方法)、方法的参数(有参方法)以及方法体中声明的局部变量,数组的每个位置称之为槽,longdouble 类型占用两个槽,其他类型(包括 this 等引用类型)占用一个槽。局部变量表的具体内容在编译成字节码文件时就已经确定,不过最大槽位数是固定的,因此为了节省空间,一旦槽中的某个局部变量不再被使用,当前槽就可以被其他局部变量复用。
    • 操作数栈:主要用于存放执行执行过程中的中间数据,在编译期就能确定操作数栈的最大深度,从而在执行时正确分配内存大小。
    • 帧数据:主要包含动态链接方法出口异常表的引用
      • 动态链接:如果当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用转换成对应的运行时常量池(位于元空间)中的内存地址,动态链接就保存了符号引用到运行时常量池中的内存地址的映射关系。
      • 方法出口:方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址,方法出口存储的就是这条指令的地址。
      • 异常表的引用:异常表存放的是代码中异常的处理信息,包含了 try 代码块的覆盖范围以及出现或未出现异常时需要跳转到的字节码指令的地址。

    10.3 本地方法栈

    本地方法栈存储的是 native 本地方法的栈帧,不过在 HotSpot 虚拟机中,Java 虚拟机栈和本地方法栈使用的是同一个栈空间。

    10.4 堆

    堆内存是空间最大的一块内存区域,创建出来的所有对象都保存在堆空间上。可以通过虚拟机参数 -Xms 修改堆的初始大小(total),通过 -Xmx 修改堆的最大大小(max)。

    堆除了存储普通的对象,还存储了字符串常量池。字符串常量池主要存储了字符串字面值,从而实现字符串的重用。可以通过 intern() 方法手动将堆中字符串的引用放入字符串常量池。

    10.5 方法区

    当虚拟机要使用一个类时,它会读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量(运行时常量池)、静态变量、即时编译器编译后的代码缓存等数据。

    方法区只是逻辑上的概念,类似于 C++ 中的自由存储区。JDK 8 以前方法区的实现为永久代,JDK8 以后其实现变为元空间。


    十一、类的生命周期

    • 加载阶段
      • 类加载器首先会通过全类名来获取此类的二进制字节码。
      • 然后 JVM 会将这个字节码所代表的静态存储结构转化为方法区的运行时数据结构。
      • 最后在堆内存中生成一个代表这个类的的 java.lang.Class 对象。
    • 验证阶段:验证 Class 文件的字节流中包含的信息是否符合 Java 虚拟机规范,主要分为文件格式验证(例如文件是否以 0xCAFEBABE 开头、主次版本号是否符合虚拟机要求)、元数据验证(例如类必须有父类)、字节码验证(例如方法内的执行指令跳转位置是否合理)、符号引用验证(例如类是否访问了其他类中的 private 方法)。
    • 准备阶段:为静态变量(类变量)分配内存并设置初始值(静态变量保存在方法区)。一般情况下准备阶段的初始值为 0,代码中的指定值会在初始化阶段再赋给静态变量,但如果这个静态变量还被 final 修饰,由于其常量性,会直接初始化为代码中的指定值。
    • 解析阶段将常量池内的符号引用替换为直接引用。直接引用不再使用编号,而是直接使用内存中的地址访问具体的数据。
    • 初始化阶段:执行静态代码块静态字段初始化语句,只有在主动使用类时才会触发类的初始化。
    • 使用阶段:包括主动引用被动引用,只有主动引用会引起类的初始化,而被动引用不会引起类的初始化。
      • 主动引用:
        • 通过 new 创建一个类的对象。
        • 访问未被 final 修饰的静态变量或者调用静态方法。
        • 调用 Class.forName(String className)
        • 虚拟机启动时会自动初始化主类,即被执行的 main() 函数所在的类。
        • 初始化一个类,如果其父类还未初始化,则先触发父类的初始化。
      • 被动引用:
        • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
        • 定义类数组。
        • 引用类的 static final 常量(常量需要为基本类型,如果为引用类型仍然会触发初始化)。
    • 卸载阶段:卸载类即将方法区中的类回收,只有同时满足以下三个条件时类才能被卸载:
      • 此类的所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类。
      • 加载该类的类加载器已经被回收。
      • 该类对应的 java.lang.Class 对象没有在任何地方被引用。

    十二、类加载

    12.1 Java 中默认的类加载器

    • 启动类加载器(BootstrapClassLoader):最顶层的加载类,由 Hotspot 虚拟机提供,通过 C++ 实现(JDK 9 之后由 Java 实现,且由按路径查找变为按模块查找)。主要用来加载 JDK 内部的核心类库(如 java.langjava.util 等)以及被 -Xbootclasspath 参数指定的路径下的所有类。
    • 扩展类加载器(ExtensionClassLoader):由 JDK 提供,主要负责加载 JDK 的扩展类库(即 $JAVA_HOME/jre/lib/ext 目录下的类,它们通用但不重要,如 javax.swingjavax.servlet 等)以及被 -Djava.ext.dirs 参数指定的路径下的所有类。
    • 应用程序类加载器(AppClassLoader):由 JDK 提供,是面向用户的加载器,主要负责加载当前应用 classpath 下的所有 jar 包和类。

    12.2 双亲委派机制

    双亲委派机制保证了类加载的安全性(所有核心类都有顶层类加载器加载,避免了恶意程序篡改核心类库),同时避免了类的重复加载

    双亲委派机制的执行流程:

    • 在类加载的时候,系统会首先判断当前类是否被加载过,已经被加载的类会直接返回。
    • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是调用父加载器 loadClass() 方法,把这个请求委派给父类加载器去完成。这样所有的请求最终都会传送到顶层的启动类加载器中。
    • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会调用自己的 findClass() 方法来尝试自己去加载。
    • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

    十三、垃圾回收

    13.1 可达性分析算法

    可达性分析算法的基本思想就是以一系列的 GC 根节点(GC Roots)对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC 根节点没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

    可作为 GC 根节点的对象主要有以下几种:

    • 线程 Thread 对象。
    • 系统类加载器加载的 java.lang.Class 对象。
    • 监视器对象,用来保存同步锁 synchronized 关键字持有的对象。
    • 本地方法调用时使用的全局对象。

    13.2 垃圾回收算法

    13.2.1 标记-清除算法

    • 标记阶段:将所有存活的对象进行标记。Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
    • 清除阶段:从内存中删除没有被标记也就是非存活对象。

    优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。

    缺点:由于内存是连续的,所以在对象被删除之后内存中会出现很多内存碎片。同时,由于内存碎片的存在,需要维护一个空闲链表对内存空间进行管理。

    13.2.2 标记-整理算法

    • 标记阶段:将所有存活的对象进行标记。Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
    • 整理阶段:将存活对象移动到堆的一端依次排列,清理掉其余空间。

    优点:不会产生内存碎片,同时内存利用率较高(相对于复制算法)。

    缺点:整理阶段会有较大的性能开销

    13.2.3 复制算法

    • 准备 From 空间和 To 空间两块空间,只能使用 From 空间进行内存分配。
    • 在垃圾回收 GC 阶段,将 From 中存活对象复制到 To 空间。
    • 将两块空间的 From 和 To 名字互换。

    优点:不会产生内存碎片,复制算法在复制时会将对象按顺序放入 To 空间,因此不存在内存碎片。

    缺点:可用内存空间会缩小为总内存空间的一半,内存利用率低。同时,如果待复制的对象过大,复制开销也会增加。

    13.2.4 分代垃圾回收算法

    分代垃圾回收将整个内存区域划分为年轻代(存放存活时间比较短的对象)和老年代(存放存活时间比较长的对象),其中年轻代还可以再分为 EdenSurvivor 0Survivor 1 三个区。

    • 创建出来的对象,首先会被放入 Eden 伊甸园区。
    • 随着 Eden 区的对象越来越多,如果 Eden 区满,继续向年轻代放入对象就会触发年轻代的 GC,称为 Minor GC 或者 Young GC。Minor GC 会把 Eden 区和 From 区中需要回收的对象回收,把没有回收的对象放入 To 区,From 区和 To 区由 Survivor 0 和 Survivor 1 轮流担任。
    • 每轮 Minor GC 都会为本轮存活对象记录一个年龄,当年龄达到 JVM 阈值时相应对象会被晋升至老年代
    • 当老年代中空间不足时,如果继续向老年代放入对象,首先会尝试 Minor GC(通过回收年轻代尝试避免继续将对象放入老年代)。如果还是不足,就会触发 Full GC,Full GC 会对整个堆进行垃圾回收。如果 Full GC 依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出 Out Of Memory 异常。

    十四、成员变量初始化顺序

    父类和子类中的成员变量初始化顺序如下:

    • 父类静态变量、静态代码块(具体顺序取决于 Java 中的编写顺序
    • 子类静态变量、静态代码块(具体顺序取决于 Java 中的编写顺序
    • 父类成员变量、构造代码块(具体顺序取决于 Java 中的编写顺序
    • 父类构造函数
    • 子类成员变量、构造代码块(具体顺序取决于 Java 中的编写顺序
    • 子类构造函数
    public class A {
    
        private static final Out outStatic = new Out("父类静态变量");
    
        static {
            System.out.println("父类静态代码块");
        }
    
        private final Out out = new Out("父类成员变量");
    
        {
            System.out.println("父类构造代码块");
        }
    
        public A() {
            System.out.println("父类构造函数");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    public class B extends A {
    
        static {
            System.out.println("子类静态代码块1");
        }
    
        private static final Out outStatic = new Out("子类静态变量");
    
        static {
            System.out.println("子类静态代码块2");
        }
    
        public B() {
            System.out.println("子类构造函数");
        }
    
        {
            System.out.println("子类构造代码块1");
        }
    
        private final Out out = new Out("子类成员变量")
    
        {
            System.out.println("子类构造代码块2");
        }
    }
    
    • 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 class Out {
        public Out(String s) {
            System.out.println(s);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    父类静态变量
    父类静态代码块
    子类静态代码块1
    子类静态变量
    子类静态代码块2
    父类成员变量
    父类构造代码块
    父类构造函数
    子类构造代码块1
    子类成员变量
    子类构造代码块2
    子类构造函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    十五、final 关键字

    Java 中的 final 关键字:

    • final 修饰变量表示该变量是一个常量,其值不可修改。常量在声明时必须初始化,一旦赋值后,就不能再次修改。
    • final 修饰方法表示该方法不能被子类重写(覆盖)。
    • final 修饰表示该类不能被继承,即不能有子类。
    • final 修饰方法参数表示该参数是只读的,不能在方法内部被修改。

    十六、接口和抽象类

    abstract 修饰的类即抽象类,一个抽象类中至少有一个抽象方法(通过关键字 abstract 定义,并且没有方法体)。从语法上来说,抽象类也可以没有抽象方法,但是这样的抽象类完全没有意义。

    接口由 interface 关键字定义,接口可以包含变量和方法,变量会被隐式指定为 public static final 且只能是 public static final,方法会被隐式指定为 public abstract 且只能是 public abstract

    接口和抽象类的共同点:

    • 均不能被实例化。
    • 都可以包含抽象方法。
    • 都可以有默认的实现,接口中可以通过 default 关键字指定默认的实现。

    接口和抽象类的区别:

    • 抽象类主要用于代码复用,强调的是 is-a 的关系,接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。比如鸟和飞机可以同时实现飞这一接口,这也意味着它们必须实现类似于起飞、降落等方法。而喷气式飞机和螺旋桨飞机可以同时继承自飞机这一抽象类,因为它们都属于飞机。
    • 抽象类是类,因此可以包含普通的方法、普通的成员变量以及构造函数等,而接口只能包含 public abstract 抽象方法和 public static final 常量。
    • Java 不支持多继承,因此一个类只能继承一个抽象类,但是一个类可以实现多个接口。

    十七、volatile

    volatile 关键字的作用:

    • 可见性:当一个变量被声明为 volatile 时,线程在读取该变量的值时,会直接从主内存中读取,而不是从线程的本地缓存中读取。同样,在写入该变量的值时,会立即写入主内存,而不是仅仅更新线程的本地缓存。这确保了当一个线程修改了变量的值时,其他线程能够立即看到这个变化。
    • 禁止指令重排序volatile 关键字还禁止了编译器和处理器对指令的重排序,从而保证在多线程环境下,特定操作的执行顺序不会被改变。这对于某些并发场景下的正确性非常重要。

    volatile 在懒汉式单例中的应用

    十八、String

    18.1 不可变性

    String 是不可变的,可以将其理解为一个常量,每次修改都会产生一个新的 String 对象,而不会直接修改原有数据,也因此是线程安全的。

    String 不可变主要有以下两点原因:

    • 保存字符串的数组被 private 修饰且没有提供任何能够直接修改底层数组的方法。
    • String 类本身被 final 修饰,从而避免了子类破坏其不可变性,

    注意String 底层数组被 final 修饰并不是其不可变的根本原因,因为 final 数组只能保证数组引用不被修改,无法保证里面的元素不能修改。

    final int[] arr = new int[]{1, 2, 3};
    arr = new int[3];  // 无法将值赋给 final 变量 'arr'
    arr[1] = 5;  // 成功执行
    System.out.println(Arrays.toString(arr));  // [1, 5, 3]
    
    • 1
    • 2
    • 3
    • 4

    18.2 String、StringBuilder、StringBuffer

    String 不可变且线程安全,但也意味着每次修改都会生成一个新的字符串对象,因此修改的性能比较低,尤其是在需要进行大量字符串修改的情况下。

    StringBuilderStringBuffer 均继承自 AbstractStringBuilder,他们的修改都是基于类本身进行的修改,因此修改性能较高。二者的唯一区别是 StringBuffer 线程安全。

    因此:

    • 操作少量的数据用 String
    • 单线程操作大量数据用 StringBuilder
    • 多线程操作大量数据用 StringBuffer

    18.3 String 的 + 和 +=

    String++= 为语法糖,底层会构造一个 StringBuilder 然后通过 append() 方法添加元素,因此对于少量的字符串拼接,使用 + 还是 StringBuilder 差别不大。

    下面代码中的两种写法完全等价:

    StringBuilder sb = new StringBuilder().append("1").append("2").append("3").append("4").append("5");
    String s = sb.toString();
    
    String s = "1" + "2" + "3" + "4" + "5";
    
    • 1
    • 2
    • 3
    • 4

    但如果是在循环中进行字符串拼接,+ 会创建大量的无用对象,效率较低,因此优先考虑使用 StringBuilder


    参考:

    https://javaguide.cn/home.html

  • 相关阅读:
    解析java中的debug模式之异常断点调试
    Google Earth Engine——使用geetool批量下载单景影像以Landsat 8 反演后的NDSI结果
    免费旋转视频
    记一次 include virtual不生效问题
    shell-whiptail代码如何实现调用新终端并执行命令运行程序
    springboot 四大组件
    后端进阶知识 MySQL为什么那么快 图文详解 之 flush 链表 与 LRU链表
    单元测试多线程调用sleep,执行中断
    黑豹程序员-架构师学习路线图-百科:Git/Gitee(版本控制)
    探索ClickHouse——同时支持导入导出功能的文件格式
  • 原文地址:https://blog.csdn.net/qq_43686863/article/details/134159465