目录
- 概述
- 语法和重要概念
- 数据类型
- 集合
- 异常
一、概述
Java语言的特点
- 面向对象:封装、继承、多态。 - 封装(Encapsulation):将数据和操作数据的方法包装在一起,隐藏内部细节,只能通过对外提供的接口,对封装在内部的属性和方法进行访问和操作。 
 继承(Inheritance):子类复用和扩展父类的属性和方法,实现层次结构。
 多态(Polymorphism):调用相同方法做出不同行为。两种实现方式:通过继承(子类方法重写)、通过接口。
- 平台无关性:JVM实现“Write Once, Run Anywhere.”。
- 可靠性:异常处理、自动内存管理机制。
- 安全性:如访问权限修饰符、限制程序直接访问操作系统资源。
编译与解释共存
.java 文件编译 为 字节码(JVM能理解的代码,即 .class 文件),字节码解释为 机器码 。由于字节码只面向JVM,因此程序无须重新编译便可在不同操作系统上运行。
- JIT(Just in Time Compilation)编译器:运行时编译。当JIT编译器完成第一次编译后,会将字节码对应的机器码保存下来,下次直接使用。
- AOT(Ahead of Time Compilation):在程序被执行前就将其编译成机器码,可以提高程序的启动速度,避免预热时间长,但无法支持反射、动态代理、动态加载、JNI(Java Native Interface)等。
二、语法和重要概念
重载和重写
- 重载(Overload):同一类中方法名相同,参数列表不同。发生在编译期。
- 重写(Override):子类重写父类方法,子类方法的返回值类型和异常比父类方法更小或相等;发生在运行期。
浅拷贝和深拷贝
- 浅拷贝:在堆上创建一个新对象,如果原对象内部属性有引用类型,浅拷贝会直接复制内部对象的引用地址,也就是说和原对象共用同一个内部对象。
- 深拷贝:会完全复制整个对象,包括其所包含的内部对象。
序列化和反序列化
位于 TCP/IP 协议中的应用层:将应用层的用户数据进行处理转换为二进制流。
- 序列化:将数据结构或对象转换成二进制字节流、JSON、XML等。
- 反序列化:将序列化过程中生成的数据转换为原始数据结构或对象的过程。
 
应用场景:网络传输、存储到文件、存储到缓存数据库、存储到内存。
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/VO等POJO类时,不要设定默认值。
【强制】所有的POJO类属性必须使用包装数据类型。
【强制】RPC方法的返回值和参数必须使用包装数据类型。
【推荐】所有的局部变量使用基本数据类型。
包装类型的缓存机制Byte ,Short ,Integer ,Long 默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。浮点数类型的包装类 Float ,Double 没有缓存机制。
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内部执行了Double的toString(),按精度对尾数进行截断。
- 保留规则: - 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():用于减少将对象加入容器(如 HashMap 、HashSet)时的 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泛型
类型擦除:编译器会对泛型代码进行类型检查,但在运行时,这些泛型的具体类型信息会被擦除,转而用原始类型(raw type,如 Object 或上界类型)来代替。
- ? extends T:上界通配符,元素是 T 或 T 的子类;取安全,存受限。
- ? super T:下界通配符,元素是 T 或 T 的父类;存安全,取受限。- List<? extends Animal> list = new ArrayList<Dog>(); Animal a = list.get(0); // OK list.add(new Dog()); // 编译错误 list.add(new Cat()); // 编译错误 list.add(new Animal()); // 编译错误 List<? super Dog> list = new ArrayList<Animal>(); list.add(new Dog()); // OK list.add(new Animal()); // 编译错误 list.add(new Cat()); // 编译错误 Object o = list.get(0); // 只能当 Object 处理
 
应用场景
- 动态指定接口返回结果的数据类型,如 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);四、集合
集合,又称为容器,由 Collection 和 Map 两大接口派生而来。
底层数据结构
- List - ArrayList:- Object[]数组。(线程不安全)
- Vector/- Stack:- Object[]数组。(线程安全)
- LinkedList:双向链表(JDK1.6以前是循环双向链表)。
 
- Set - HashSet:哈希表(基于- HashMap)。
- LinkedHashSet:继承- HashSet,链表+哈希表(基于- LinkedHashMap)。
- TreeSet:红黑树(自平衡的排序二叉树)。
 
- Queue - PriorityQueue:- Object[]数组实现小顶堆。
- DelayQueue:基于- PriorityQueue。
- ArrayDeque:基于数组、可动态扩容的双端队列。
 
- 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默认为当前容量oldCapacity的1.5倍。
- 若 minCapacity > newCapacity,直接将新容量设置为minCapacity。
- 若 minCapacity > MAX_ARRAY_SIZE,直接将新容量设置为Integer.MAX_VALUE。
 
ArrayList 指定位置插入
通过 System.arraycopy(elementData, index, elementData, index + 1, size - index) 将插入位置后的元素后移一位,再通过 elementData[index] = element 插入。
CopyOnWriteArrayList
采用写时复制的机制,在写操作时,先加写锁,创建一个原集合的副本(即拷贝一份原始数组),然后在副本上进行修改操作,最后将修改后的副本替换原集合,释放写锁。这样可以保证在写操作期间,其他线程仍然可以安全地读取原集合的数据(读操作返回的结果可能不是最新的),不会受到并发修改的影响。此外,CopyOnWriteArrayList 通过 volatile 关键字保证切换过程对读线程立即可见。
- 优点:无需加锁或同步就能使多个线程同时高效读取,适用于读多写少的并发场景。
- 缺点:读操作无法立即反映最新修改;每次写操作都会创建新数组,占用内存。
Set
自定义排序
- Comparator - Collections.sort(arrayList, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } });
- 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 是双端队列,两端都可以插入或删除元素。下表中,左边的方法失败后抛出异常,右边则返回特殊值。
| 操作 | Queue | Deque | 
|---|---|---|
| 插入队尾 | 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 的异同
- ArrayDeque和- LinkedList都实现了- Deque接口,都具有队列的功能。
- ArrayDeque基于动态循环数组和双指针实现,- LinkedList通过双向链表实现。
- ArrayDeque不能存储- null,需要用其标识未使用的槽位,而- LinkedList可以。
- ArrayDeque插入时可能存在扩容过程, 均摊后的插入操作依然为- O(1)。虽然- LinkedList不需要扩容,但每次插入数据时均需要申请新的堆空间,均摊性能更慢。
- ArrayDeque可以用于实现栈。
 
PriorityQueue
利用了二叉堆(默认小顶堆)的数据结构来实现的,通过可变长的 Object[] 数组实现,能够在 O(log N) 的时间复杂度下插入元素和删除堆顶元素。
堆排序:215. 数组中的第K个最大元素。
 
BlockingQueueBlockingQueue (阻塞队列)是一个接口,常用于生产者-消费者模型。当队列没有元素时一直阻塞,直到有元素;若队列已满,等到队列能放入新元素时再放入。
BlockingQueue 有以下实现类:
- ArrayBlockingQueue:使用数组实现的有界阻塞队列。创建时需要指定容量大小,需要提前分配数组内存,并支持公平和非公平两种方式的锁访问机制。
- LinkedBlockingQueue:使用单向链表实现的可选有界阻塞队列。创建时可以指定容量大小,不指定则默认为- Integer.MAX_VALUE,根据元素的增加而逐渐占用内存空间,仅支持非公平的锁访问机制。- ArrayBlockingQueue的锁是没有分离的,即生产和消费用的是同一个锁;而- LinkedBlockingQueue的锁是分离的,即生产用- putLock,消费是- takeLock,能够防止生产者和消费者线程之间的锁争夺。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。元素实现- Comparable接口或在构造函数中传入- Comparator对象,不能插入- null元素。
- SynchronousQueue:同步队列,一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,删除操作也必须等待插入操作。通常用于线程间直接传递数据。
- DelayQueue:延迟队列,元素只有到了其指定的延迟时间,才能够从队列中出队。
Map
HashMap 和 Hashtable 的区别
- 线程安全:HashMap是非线程安全的,Hashtable是线程安全的。这是因为Hashtable内部的方法基本都经过synchronized修饰。(要保证线程安全可以用ConcurrentHashMap)
- 效率:因为线程安全问题,HashMap要比Hashtable效率高。
- Null 值:HashMap可以存储null的key(一个)或value(多个),Hashtable不可以。
- 容量大小:HashMap默认大小为16,每次扩充为原来的2倍;Hashtable默认大小为11,每次扩充变为原来的2n+1。若创建时给定大小,HashMap会将其扩充为2的幂次方大小,Hashtable会直接使用给定的大小。
- 哈希函数:HashMap对哈希值进行了高位和低位的混合扰动处理以减少冲突;Hashtable直接使用哈希值。
- 底层数据结构:JDK1.8 以后的 HashMap在解决哈希冲突时,当链表长度大于阈值(默认为8)时,会将链表转化为红黑树(若哈希桶数量小于64,则会进行数组扩容和rehash,而不是转换为红黑树)以减少搜索时间。Hashtable没有这样的机制。 
HashMap 和 TreeMap 的区别
都继承自 AbstractMap ,TreeMap 还实现了 NavigableMap 和 SortedMap 接口。实现 SortedMap 接口让其有了对集合中的元素根据键排序的能力;实现 NavigableMap 接口让其有了对集合内元素的搜索的能力:
- 定向搜索:ceilingEntry(),floorEntry(),higherEntry()和lowerEntry()可以用于定位大于等于、小于等于、严格大于、严格小于给定键的最接近的键值对。
- 子集操作:subMap(),headMap()和tailMap()方法可以高效地创建原集合的子集视图,而无需复制整个集合。
- 逆序视图:descendingMap()方法返回一个逆序的NavigableMap视图,使得可以反向迭代整个TreeMap。
- 边界操作: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:受检查异常,没有被 catch或throws关键字处理的话无法通过编译。
- 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
文章标题:【八股】Java
文章作者:nek0peko
文章链接:https://nek0peko.com/index.php/archives/151/
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,未经站长允许不得对文章文字内容进行修改演绎。
本文采用创作共用保留署名-非商业-禁止演绎4.0国际许可证