目录
- 概述
- 语法和重要概念
- 数据类型
- 集合
- 异常
一、概述
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
泛型
应用场景
- 动态指定接口返回结果的数据类型,如
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
插入。
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国际许可证