目录

  • 概述
  • 语法和重要概念
  • 数据类型
  • 集合
  • 异常

一、概述

Java语言的特点

  1. 面向对象:封装、继承、多态。

    封装(Encapsulation):将数据操作数据的方法包装在一起,隐藏内部细节,只能通过对外提供的接口,对封装在内部的属性和方法进行访问和操作。
    继承(Inheritance):子类复用和扩展父类的属性和方法,实现层次结构。
    多态(Polymorphism):调用相同方法做出不同行为。两种实现方式:通过继承(子类方法重写)、通过接口。
  2. 平台无关性:JVM实现“Write Once, Run Anywhere.”。
  3. 可靠性:异常处理、自动内存管理机制。
  4. 安全性:如访问权限修饰符、限制程序直接访问操作系统资源。

编译与解释共存

.java 文件编译字节码(JVM能理解的代码,即 .class 文件),字节码解释机器码 。由于字节码只面向JVM,因此程序无须重新编译便可在不同操作系统上运行。
编译与解释共存

  • JIT(Just in Time Compilation)编译器:运行时编译。当JIT编译器完成第一次编译后,会将字节码对应的机器码保存下来,下次直接使用。
  • AOT(Ahead of Time Compilation):在程序被执行前就将其编译成机器码,可以提高程序的启动速度,避免预热时间长,但无法支持反射、动态代理、动态加载、JNI(Java Native Interface)等。

二、语法和重要概念

重载和重写

  • 重载(Overload):同一类中方法名相同,参数列表不同。发生在编译期。
  • 重写(Override):子类重写父类方法,子类方法的返回值类型和异常比父类方法更小或相等;发生在运行期。

浅拷贝和深拷贝

  • 浅拷贝:在堆上创建一个新对象,如果原对象内部属性有引用类型,浅拷贝会直接复制内部对象的引用地址,也就是说和原对象共用同一个内部对象
  • 深拷贝:会完全复制整个对象,包括其所包含的内部对象。

序列化和反序列化

位于 TCP/IP 协议中的应用层:将应用层的用户数据进行处理转换为二进制流。

  • 序列化:将数据结构或对象转换成二进制字节流、JSONXML 等。
  • 反序列化:将序列化过程中生成的数据转换为原始数据结构或对象的过程。

 
应用场景:网络传输、存储到文件、存储到缓存数据库、存储到内存。

JDK自带的序列化方式:实现 java.io.Serializable 接口。

  • serialVersionUID 用于标识类的序列化版本,没有被序列化。
  • static 修饰的变量因为不属于任何对象,所以不会被序列化。
  • transient 修饰的变量不会被序列化,反序列化后会被置成类型默认值。
  • 缺点:不支持跨语言调用、性能差、安全问题。
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class RpcRequest implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String requestId;
    ......
}

其它序列化方式:Kryo、Protobuf、ProtoStuff、Hessian。


强引用/软引用/弱引用/虚引用

类型特点典型用途
强引用普通的对象引用,垃圾回收器不会回收。常规对象使用。
软引用内存不足时回收对象;可以和引用队列联合使用。缓存数据(如图片缓存)。
弱引用不论内存是否充足,GC时都会回收;可以和引用队列联合使用。缓存、引用池中的对象引用。
虚引用不能访问对象,用于跟踪对象回收;必须和引用队列联合使用。监控对象回收、清理资源。

反射 / 注解


代码块执行顺序

class Test {
    {
        System.out.println("普通代码块");
    }
    static {
        System.out.println("静态代码块(只执行一次)");
    }
    public Test() {
        System.out.println("构造函数");
    }
}

public class Main {
    public static void main(String[] args) {
        Test t1 = new Test();
        Test t2 = new Test();
    }
}

顺序:静态代码块(只执行一次)->普通代码块->构造函数->普通代码块->构造函数

值传递

Java只有值传递,没有引用传递,方法接收的是实参值的拷贝(可以是实参的地址),会创建副本。


三、数据类型

包装类型

包装类型与基本类型的区别

  • 用途:除了定义一些常量和局部变量之外,方法参数、对象属性中常用包装类型。包装类型可用于泛型,而基本类型不行。
  • 存储方式基本类型的局部变量存放在的局部变量表中,成员变量(未被 static 修饰)存放在中。包装类型属于对象类型,因此存在中。
  • 占用空间:基本类型占用的空间往往更小。
  • 默认值:基本类型有默认值,而包装类型为 null
  • 比较方式:因为包装类型是对象,== 比较的是内存地址,equals() 比较的是值。

 

阿里Java开发手册:
【强制】定义 DO/DTO/VOPOJO 类时,不要设定默认值。
【强制】所有的 POJO 类属性必须使用包装数据类型。
【强制】RPC 方法的返回值和参数必须使用包装数据类型。
【推荐】所有的局部变量使用基本数据类型。

包装类型的缓存机制
ByteShortIntegerLong 默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。浮点数类型的包装类 FloatDouble 没有缓存机制。

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2); // true

Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22); // false

自动装箱和拆箱
频繁拆装箱会严重影响系统性能,应该尽量避免不必要的拆装箱操作。

Integer i = 10;                   // 装箱
Integer i = Integer.valueOf(10);  // 与上面等价
int n = i;                        // 拆箱
int n = i.intValue();             // 与上面等价

BigInteger

内部通过 int[] 实现,用于超过 long 的64位的数据,运算效率较低。


BigDecimal

用于解决浮点数运算精度丢失问题(如涉及钱的场景)。

  • 创建:为防止精度丢失,不要使用构造方法 BigDecimal(double) ,推荐使用 BigDecimal(String val)BigDecimal.valueOf(double val)valueOf 内部执行了 DoubletoString() ,按精度对尾数进行截断。
  • 保留规则RoundingMode 中有非常多规则可供选择。

    public enum RoundingMode {
      // 2.5 -> 3, 1.6 -> 2, -1.6 -> -2, -2.5 -> -3
      UP(BigDecimal.ROUND_UP),
      // 2.5 -> 2, 1.6 -> 1, -1.6 -> -1, -2.5 -> -2
      DOWN(BigDecimal.ROUND_DOWN),
      // 2.5 -> 3, 1.6 -> 2, -1.6 -> -1, -2.5 -> -2
      CEILING(BigDecimal.ROUND_CEILING),
      // 2.5 -> 2, 1.6 -> 1, -1.6 -> -2, -2.5 -> -3
      FLOOR(BigDecimal.ROUND_FLOOR),
      // 2.5 -> 3, 1.6 -> 2, -1.6 -> -2, -2.5 -> -3
      HALF_UP(BigDecimal.ROUND_HALF_UP),
      ......
    }

    四舍六入五成双:对应 RoundingMode.HALF_EVEN ,防止结果偏向大数。
    例:1.51 -> 1.6,1.5 -> 2,2.5 -> 2,-1.5 -> -2,-2.5 -> -2。
    5后有数字时:舍5入1。
    5后无数字时:前数为奇数,舍5入1;前数为偶数,舍5不进。

  • 保留几位小数setScale()

    BigDecimal a = a.setScale(3, RoundingMode.HALF_DOWN);
  • 加减乘除add()subtract()multiply()divide()

    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("0.9");
    BigDecimal result;
    result = a.add(b);       // 1.9
    result = a.subtract(b);  // 0.1
    result = a.multiply(b);  // 0.90
    result = a.divide(b);    // 无法除尽,抛出 ArithmeticException
    result = a.divide(b, 2, RoundingMode.HALF_UP);  // 1.11,保留2位小数
  • 比较方式:浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals() 来判断。

    float a = 1.0F - 0.9F;
    float b = 0.9F - 0.8F;
    System.out.println(a == b);                   // False
    System.out.println(Math.abs(a - b) < 1e-6F);  // True
    
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("0.9");
    BigDecimal c = new BigDecimal("0.8");
    BigDecimal x = a.subtract(b);
    BigDecimal y = b.subtract(c);
    System.out.println(x.compareTo(y));  // 0

Object

== 与 equals()的区别

  • 对于基本数据类型,== 比较值;对于引用数据类型,== 比较对象的内存地址。
  • equals() 不能用于基本类型,只能用来判断对象是否相等,重写前等价于 ==

 
Object.HashCode():用于减少将对象加入容器(如 HashMapHashSet)时的 equals() 次数:如果发现已经有 hashcode 相同的对象,就调用 equals() 来检查它们是否真的相等,如果相同则加入失败。

  • 两个有相同的 hashCode 的对象不一定是相等的(哈希碰撞)。
  • 重写 equal() 时必须重写 HashCode() ,两个相等的对象 hashcode 也要相等。

String

  • String:不可变,因为底层是 final 修饰的值。(Java 9后,对只包含ASCII字符的字符串,从2个字节的 char[] 换成了 byte[]
  • StringBuilder:可变,线程不安全。
  • StringBuffer:可变,线程安全,适用于多线程。

 
字符串常量池:通过 native (本地) 方法 String.intern() 向常量池中添加新内容字符串的引用,以及返回常量池中相同内容对象的引用,从而避免字符串的重复创建。

// 在字符串常量池中创建字符串对象 "aa",将其引用赋值给 s1
String s1 = "aa";
// 直接返回字符串常量池中字符串对象 "aa",将其引用赋值给 s2
String s2 = "aa";
System.out.println(s1 == s2); // true

// 创建2个对象,先在常量池中创建,再由 new String() 在堆中创建,使用常量池中的 "ab" 初始化
String s3 = new String("ab");
// 创建1个对象,由 new String() 在堆中创建,使用常量池中的 "ab" 初始化
String s4 = new String("ab");

字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";    // 常量池中的对象
String d = str1 + str2;      // 常量池中的对象
System.out.println(c == d);  // true

泛型

应用场景

  • 动态指定接口返回结果的数据类型,如 CommonResult<T>
  • 定义 Excel 处理类 ExcelUtil<T> 用于动态指定导出的数据类型。
  • 构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。

 
1. 泛型类

public class MyClass<T>{
    private T key;
    public MyClass(T key) {
        this.key = key;
    }
}
// 在实例化泛型类时,必须指定具体类型
MyClass<Integer> myClassInteger = new MyClass<>(123456);

2. 泛型接口

public interface Xxxx<T> {
    public T method();
}
// 实现泛型接口,不指定类型
class XxxxImpl<T> implements Xxxx<T> {
    @Override
    public T method() {
        ......
    }
}
// 实现泛型接口,指定类型
class XxxxImpl implements Xxxx<String> {
    @Override
    public String method() {
        ......
    }
}

3. 泛型方法

public static <E> void method(E[] inputArray) {
    for (E element : inputArray){
        ......
    }
}
// Usage
Integer[] intArray = {1, 2, 3};
String[] stringArray = {"Hello", "World"};
method(intArray);
method(stringArray);

四、集合

集合,又称为容器,由 CollectionMap 两大接口派生而来。
Java2
底层数据结构

  1. List

    • ArrayListObject[] 数组。(线程不安全)
    • Vector / StackObject[] 数组。(线程安全)
    • LinkedList:双向链表(JDK1.6以前是循环双向链表)。

 

  1. Set

    • HashSet:哈希表(基于 HashMap)。
    • LinkedHashSet:继承 HashSet ,链表+哈希表(基于 LinkedHashMap)。
    • TreeSet:红黑树(自平衡的排序二叉树)。

 

  1. Queue

    • PriorityQueueObject[] 数组实现小顶堆。
    • DelayQueue:基于 PriorityQueue
    • ArrayDeque:基于数组、可动态扩容的双端队列。

 

  1. Map

    • HashMap:数组+链表(拉链法),链表长度大于阈值时会转化为红黑树。
    • LinkedHashMap:继承自 HashMap ,增加了一条双向链表实现有序。
    • Hashtable:数组+链表。
    • TreeMap:红黑树(自平衡的排序二叉树)。
    • ConcurrentHashMap:线程安全。
    • WeakHashMap:基于弱引用的 HashMap,用于实现缓存。

List

ArrayList 与 Array 的区别

  • 容量ArrayList 可根据存储的元素动态扩容或缩容,并支持 add()remove() 等操作;Array 创建时需指定大小。
  • 类型ArrayList 只能存储对象,对于基本类型需要使用其对应的包装类;Array 既可以存储基本类型,也可以存储对象。
  • 泛型ArrayList 可以用泛型来确保类型安全,Array 不行。

 
ArrayList 与 LinkedList 的区别

  • 底层数据结构:分别为 Object[] 数组和双向链表。
  • 随机访问ArrayList 能通过序号快速获取对象,LinkedList 不行。
  • 插入和删除ArrayList 在指定位置插入时间复杂度为 O(n) ,默认的尾插为 O(1)LinkedList 在指定位置插入为 O(n) ,头插和尾插为 O(1)
  • 内存占用:每个 ArrayList 会有一定的预留容量空间;LinkedList 每个元素都要消耗更多的空间。

 
ArrayList 扩容机制
ArrayList 通过无参构造函数创建时,默认容量为 10 。当执行 add() 操作容量不足时,会触发自动扩容,假设需要的最小容量为 minCapacity

  • 扩容时,新容量 newCapacity 默认为当前容量 oldCapacity1.5 倍。
  • minCapacity > newCapacity,直接将新容量设置为 minCapacity
  • minCapacity > MAX_ARRAY_SIZE ,直接将新容量设置为 Integer.MAX_VALUE

 
ArrayList 指定位置插入
通过 System.arraycopy(elementData, index, elementData, index + 1, size - index) 将插入位置后的元素后移一位,再通过 elementData[index] = element 插入。


Set

自定义排序

  1. Comparator

    Collections.sort(arrayList, new Comparator<Integer>() {
     @Override
     public int compare(Integer o1, Integer o2) {
         return o2.compareTo(o1);
     }
    });
  2. Comparable

    @Getter
    public class Person implements Comparable<Person> {
     private int age;
    
     @Override
     public int compareTo(Person o) {
         return Integer.compare(this.age, o.getAge());
     }
    }
    
    TreeMap<Person, String> pdata = new TreeMap<Person, String>();

HashSet、LinkedHashSet 和 TreeSet 的异同

  • 相同点:都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • 底层数据结构:见前文。
  • 应用场景HashSet 用于不需要保证元素插入和取出顺序的场景;LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

Queue

Queue 与 Deque 的区别
Queue 是单端队列,只能从一端插入元素,另一端删除元素;Deque 是双端队列,两端都可以插入或删除元素。下表中,左边的方法失败后抛出异常,右边则返回特殊值。

操作QueueDeque
插入队尾add(E e) / offer(E e)addLast(E e) / offerLast(E e)
插入队首-addFirst(E e) / offerFirst(E e)
删除队首remove() / poll()removeFirst() / pollFirst()
删除队尾-removeLast() / pollLast()
查询队首元素element() / peek()getFirst() / peekFirst()
查询队尾元素-getLast() / peekLast()

 
ArrayDeque 与 LinkedList 的异同

  • ArrayDequeLinkedList 都实现了 Deque 接口,都具有队列的功能。
  • ArrayDeque 基于动态循环数组和双指针实现,LinkedList 通过双向链表实现。
  • ArrayDeque 不能存储 null,需要用其标识未使用的槽位,而 LinkedList 可以。
  • ArrayDeque 插入时可能存在扩容过程, 均摊后的插入操作依然为 O(1) 。虽然 LinkedList 不需要扩容,但每次插入数据时均需要申请新的堆空间,均摊性能更慢。
  • ArrayDeque 可以用于实现栈。

 
PriorityQueue
利用了二叉堆(默认小顶堆)的数据结构来实现的,通过可变长的 Object[] 数组实现,能够在 O(log N) 的时间复杂度下插入元素和删除堆顶元素。

堆排序215. 数组中的第K个最大元素

 
BlockingQueue
BlockingQueue (阻塞队列)是一个接口,常用于生产者-消费者模型。当队列没有元素时一直阻塞,直到有元素;若队列已满,等到队列能放入新元素时再放入。

BlockingQueue 有以下实现类:

  1. ArrayBlockingQueue:使用数组实现的有界阻塞队列。创建时需要指定容量大小,需要提前分配数组内存,并支持公平和非公平两种方式的锁访问机制。
  2. LinkedBlockingQueue:使用单向链表实现的可选有界阻塞队列。创建时可以指定容量大小,不指定则默认为 Integer.MAX_VALUE,根据元素的增加而逐渐占用内存空间,仅支持非公平的锁访问机制。

    ArrayBlockingQueue 的锁是没有分离的,即生产和消费用的是同一个锁;而 LinkedBlockingQueue 的锁是分离的,即生产用 putLock,消费是 takeLock ,能够防止生产者和消费者线程之间的锁争夺。
  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。元素实现 Comparable 接口或在构造函数中传入 Comparator 对象,不能插入 null 元素。
  4. SynchronousQueue:同步队列,一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,删除操作也必须等待插入操作。通常用于线程间直接传递数据。
  5. DelayQueue:延迟队列,元素只有到了其指定的延迟时间,才能够从队列中出队。

Map

HashMap 和 Hashtable 的区别

  • 线程安全HashMap 是非线程安全的,Hashtable 是线程安全的。这是因为 Hashtable 内部的方法基本都经过 synchronized 修饰。(要保证线程安全可以用 ConcurrentHashMap
  • 效率:因为线程安全问题,HashMap 要比 Hashtable 效率高。
  • Null 值HashMap 可以存储 nullkey(一个)或 value(多个),Hashtable 不可以。
  • 容量大小HashMap 默认大小为16,每次扩充为原来的2倍;Hashtable 默认大小为11,每次扩充变为原来的 2n+1 。若创建时给定大小,HashMap 会将其扩充为2的幂次方大小,Hashtable 会直接使用给定的大小。
  • 哈希函数HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突;Hashtable 直接使用哈希值。
  • 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时,当链表长度大于阈值(默认为8)时,会将链表转化为红黑树(若哈希桶数量小于64,则会进行数组扩容和 rehash ,而不是转换为红黑树)以减少搜索时间。Hashtable 没有这样的机制。
    [Java3

HashMap 和 TreeMap 的区别
都继承自 AbstractMapTreeMap 还实现了 NavigableMapSortedMap 接口。实现 SortedMap 接口让其有了对集合中的元素根据键排序的能力;实现 NavigableMap 接口让其有了对集合内元素的搜索的能力:

  1. 定向搜索ceilingEntry()floorEntry()higherEntry()lowerEntry() 可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。
  2. 子集操作subMap()headMap()tailMap() 方法可以高效地创建原集合的子集视图,而无需复制整个集合。
  3. 逆序视图descendingMap() 方法返回一个逆序的 NavigableMap 视图,使得可以反向迭代整个 TreeMap
  4. 边界操作firstEntry()lastEntry()pollFirstEntry()pollLastEntry() 等方法可以方便地访问和移除元素。

这些方法都是基于红黑树数据结构的属性实现的,红黑树保持平衡状态,从而保证了搜索操作的时间复杂度为 O(log N)


五、异常

异常种类

Exception 和 Error:都继承自 Throwable 类。

  • Exception:程序本身可以处理的异常,可以通过 catch 捕获。
  • Error:程序无法处理的错误,如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,JVM一般会终止线程。

 
Checked Exception 和 Unchecked Exception

  • checked Exception:受检查异常,没有被 catchthrows 关键字处理的话无法通过编译。
  • Unchecked Exception:不受检查异常,如 NullPointerException 等。

异常捕获

try-catch-finally 和 try-with-resources

  • finally 块除线程死亡、CPU关闭、虚拟机终止等特殊情况外都会执行,即使 try 块或 catch 块中遇到 return 语句,也会在方法返回之前执行。
  • 如果在 finally 块中使用 return ,会忽略掉 try 块中的 return
  • 不要把异常定义为静态变量,这样为导致异常栈信息错乱。每次手动抛出异常都需要 new 一个异常对象。
  • 面对必须要关闭的资源,总是优先使用 try-with-resources

    Scanner scanner = null;
    try {
      scanner = new Scanner(new File("test.txt"));
      ......
    } catch (FileNotFoundException fe) {
      fe.printStackTrace();
    } finally {
      if (scanner != null) {
          scanner.close();
      }
    }
    
    // try-with-resources
    try (Scanner scanner1 = new Scanner(new File("test1.txt"));
         Scanner scanner2 = new Scanner(new File("test2.txt"))) {
      ......
    } catch (FileNotFoundException fe) {
      fe.printStackTrace();
    }

参考资料:JavaGuide

2024-11-29 技术学习·none