Java圣经

Java圣经

Java是“半编译半解释”的语言

  • 编译:.java需要编译成.class才能执行(性能高)
  • 解释:.class需要JVM解释才能运行(跨平台性好)

此外,JVM 中的即时编译器(Just-In-Time Compiler, JIT)可以选择性地将一些经常执行的字节码编译成本地机器码,以提高执行效率。

基础篇

面向对象三特性

封装、继承、重载

重载

方法的返回值类型不会影响方法的重载。区分重载的方法的关键在于方法的名字以及参数列表的不同。

1
2
3
4
5
6
private int getMax(int maxIndex) {

}
private String getMax(int maxIndex,int maxLen) {

}

访问修饰符

1
2
3
4
int x =0; // package-private 同一个包下的所有子类都可以访问
public int x = 0; —— 公开访问。
private int x = 0; —— 私有访问。
protected int x = 0; —— 受保护访问(只要在同一个父包下都可以访问)。

子类能访问父类的私有变量吗?

1
不能,子类访问父类变量的操作完全受限于变量修饰符

常见类

Object

  • wait:线程进入等待状态,而不是阻塞状态,只能通过notify或者notifyAll唤醒
  • notify:随机唤醒一个wait线程
  • notifyAll:唤醒所有wait线程

String

String s = "123";形式的字符串都会加载到字符串常量池,而使用new创建的是对象,所以地址指向不同

  • intern():将字符串对象内容加入到字符串常量池,返回该字符串池中字符串的引用
1
2
3
4
5
6
String s = "123";
public static void main(String[] args) {
String s2 = "123";
String s3 = new String("123").intern();
System.out.println(new Test().s == s3);
}

Random

  • nextInt(i) 随机输出0 ~(i-1)的整数
1
2
3
4
5
6
public static void main(String[] args){
for (int i = 0; i < 10; i++){
int j = new Random().nextInt(3);
System.out.println(j);
}
}

Thread

Thread类为线程抽象类,常用方法start(),run()

  1. run()方法是Thread类线程启动时自动启动的任务
  2. start()方法作用是开启一个新线程

Runnable类可以将执行任务和线程启动分离开来,实现减耦合

1
2
3
4
5
6
7
8
9
10
11
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
t.start();
}
}
  1. interrupt()方法:

当一个线程通过 interrupt() 方法被中断时,它会收到一个中断信号,但不会立即停止执行。具体的中断响应需要在线程的代码中进行处理

中断线程,需要注意的是,此时线程如果在sleep、wait状态下会抛出中断异常

本质是在while循环中判断Thread.interrupted()标志,当被中断标志将被设置为true,从而跳出循环执行设定的中断逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javaCopy codeThread myThread = new Thread(() -> {
try {
while (!Thread.interrupted()) {
// 线程的业务逻辑
Thread.sleep(1000); // 可能抛出InterruptedException
}
} catch (InterruptedException e) {
// 处理中断异常
}
// 线程被中断后的处理
});

// 启动线程
myThread.start();

// 中断线程
myThread.interrupt();
  1. yield()方法:

暂停当前正在执行的线程对象,并执行其他线程。

Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。

打个比方:现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”,然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗? 不一定的,他们只是概率上大些,也有可能没特权的抢到了。

1
thread.yield();

Stream

中间操作:过滤、映射、排序

终端操作:遍历、收集、分组收集、聚合(max-min-count)

Stream可以由数组或集合创建,对流的操作分为两种:

  1. 中间操作,每次返回一个新的流,可以有多个。(筛选filter、映射map、排序sorted、去重组合skip—limit

  2. 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。(遍历foreach、匹配find–match、规约reduce、聚合max–min–count、收集collect

instanceof

instanceof 概念在多态中引出,因为在多态发生时,子类只能调用父类中的方法(编译时类型的方法),而子类自己独有的方法(运行时类型的方法)无法调用,如果强制调用的话就需要向下转型,语法和基本类型的强制类型转换一样;但是向下转型具有一定的风险,很有可能无法成功转化,为了判断能否成功转化,就需要 instanceof 先进行一个判断,然后再进行转换操作;

1
2
3
4
5
6
7
8
Object test = "Hello"; // test实际类型是String,但是Object是所有类的父类
System.out.println(test instanceof Object); // 返回true ,因为test编译时时Object类,test可以是Object类实例
System.out.println(test instanceof String); // 返回true ,因为Object是String的父类,test可以是String类的实例
System.out.println(test instanceof Math); // 返回false ,因为Object是Math的父类,但是test不是Math类的实例

// 不符合instanceof语法规则:
String test02 = "Hello"; // test02是String类
System.out.println(test02 instanceof Math); // 编译出错,String类和Math类无继承关系

String和StringBuilder和StringBuffer

区别

34b66804041da10f2116443cfba7bcc1

StringStringBuilderStringBuffer 都是 Java 中用于处理字符串的类,但它们之间有一些重要的区别。下面我将详细解释每个类及其特点:

继承关系

15ce9a00da38da88404698d41e79e93d

  1. String

String 是 Java 中不可变的字符串类。这意味着一旦创建了一个 String 对象,就不能修改它。每次对 String 对象进行修改(例如,使用 + 连接字符串)时,都会创建一个新的 String 对象。因此,在处理大量字符串修改操作时,使用 String 可能会导致性能问题和大量的内存消耗。

示例:

1
2
String str1 = "Hello";
String str2 = str1 + " World"; // 创建一个新的String对象
  1. StringBuilder

StringBuilder 是 Java 中一个可变的字符串构建器类。与 String 不同,StringBuilder 允许你在不创建新对象的情况下修改字符串。这使得 StringBuilder 在处理大量字符串修改操作时更加高效。

StringBuilder 是线程不安全的,因此在单线程环境中使用它是更好的选择。

示例:

1
2
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 修改原有的StringBuilder对象
  1. StringBuffer

StringBuffer 类似于 StringBuilder,也是一个可变的字符串构建器类。与 StringBuilder 的主要区别在于,StringBuffer 是线程安全的,这意味着它可以在多线程环境中安全地使用。然而,由于线程安全的实现需要额外的开销,因此在单线程环境中,StringBuilder 通常比 StringBuffer 更快。

示例:

1
2
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World"); // 修改原有的StringBuffer对象

总结

  • String:不可变,每次修改都会创建新对象,适用于字符串较少修改的场景。
  • StringBuilder:可变,线程不安全,适用于单线程环境中大量字符串修改的场景。
  • StringBuffer:可变,线程安全,适用于多线程环境中大量字符串修改的场景。

在选择使用哪个类时,应根据具体的应用场景和需求来决定。如果需要频繁地修改字符串,并且是在单线程环境中,那么 StringBuilder 通常是一个更好的选择。如果在多线程环境中需要修改字符串,那么应该使用 StringBuffer。而如果只是简单地存储和传递字符串,不进行修改,那么 String 就足够了。

为什么StringBuffer是线程安全的

本质内部使用了锁同步机制,在同一时间只能有一个线程修改资源

StringBuffer是线程安全的,主要得益于其内部实现了同步机制。在多线程环境下,多个线程可能同时访问和修改同一个StringBuffer实例。为了保证线程安全,StringBuffer的方法都是同步的,这意味着当一个线程正在执行StringBuffer的某个方法时,其他线程必须等待,直到这个方法执行完毕并释放锁,其他线程才能继续访问。这种同步机制确保了同一时刻只有一个线程可以执行StringBuffer的操作,从而避免了多线程之间的竞态条件,保证了线程安全。

另外,StringBuffer是可变对象,但其可变性是通过同步来控制的。这允许在原有字符串的基础上进行修改,而不需要创建新的对象,提高了在处理大量字符串拼接时的效率。

需要注意的是,虽然StringBuffer是线程安全的,但在单线程环境中,使用StringBuilder可能获得更好的性能,因为StringBuilder没有同步机制的开销。而在多线程环境中,为了确保数据的一致性和完整性,应使用StringBuffer。

总结来说,StringBuffer的线程安全性主要归功于其同步方法的设计和实现,这确保了多线程环境下对StringBuffer的访问和修改都是安全的。

集合类

继承关系

Collection

Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序

2243690-9cd9c896e0d512ed

集合的遍历

  1. 迭代器iterator

注意:Java 迭代器是一种单向遍历机制,即只能从前往后遍历集合中的元素,不能往回遍历。同时,在使用迭代器遍历集合时,不能直接修改集合中的元素,而是需要使用迭代器的 remove() 方法来删除当前元素。

原因:因为collection内部维护了一个modCount,而迭代器类维护了一个expectedModCount,如果使用集合API操作集合将会导致两者不一样,这样会报错。之所以迭代器调用remove方法可以避免不一样是因为在移除后会更新expectedModCount

1
2
3
4
5
6
// 获取迭代器
Iterator<String> it = list.iterator();
// 输出集合中的所有元素
while(it.hasNext()) {
System.out.println(it.next());
}
  1. foreach
1
list.forEach(System.out::println);
  1. 增强for
1
2
3
for (String s : list) {
System.out.println(s);
}

List

ArrayList

  • 概述ArrayList 是基于动态数组实现的列表。它允许随机访问,因为内部使用数组存储元素。
  • 特点
    • 随机访问:支持快速的随机访问,时间复杂度为 O(1)。
    • 插入和删除:在列表中间插入或删除元素的时间复杂度为 O(n),因为需要移动后续元素。

LinkedList

  • 概述:LinkedList 是基于双向链表实现的列表。每个元素都有前驱和后继指针。
  • 特点
    • 插入和删除:在列表中间插入或删除元素的时间复杂度为 O(1),前提是已经定位到该位置。
    • 随机访问:随机访问的时间复杂度为 O(n),因为需要从头或尾遍历到目标位置。

Vector

Vector 也是基于动态数组实现的列表,类似于 ArrayList,但它是线程安全

Set

HashSet

HashSet 是基于哈希表实现的集合,不允许重复元素

LinkedHashSet

LinkedHashSet HashSet 的子类,维护了一个双向链表来记录元素的插入顺序。

不同点LinkedHashSet 相对于HashSet保持元素的插入顺序

TreeSet

TreeSet 是基于红黑树实现的集合,对Key进行排序,不允许重复元素

Map

  • HashMap:基于哈希表实现,具有快速的查找和插入操作,适用于需要快速查找键值对的场景。
  • TreeMap:基于红黑树实现,可以对键进行排序,并提供了一系列与排序相关的方法,适用于需要对键进行排序的场景。
  • LinkedHashMap:基于哈希表和链表实现,保持键值对的插入顺序,适用于需要保持插入顺序的场景。

HashMap

HashCode

相同对象计算的HashCode值相同,所以可以拿来当做地址用来检索数据

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HashCodeTest {
public static void main(String[] args) {
int hash= 0;
String s= "ok";
StringBuilder sb = new StringBuilder(s);

System.out.println(s.hashCode() + " " + sb.hashCode());

String t = new String("ok");
StringBuilder tb =new StringBuilder(s);
System.out.println(t.hashCode() + " " + tb.hashCode());
}
}
1
2
3
运行结果:
3548 1829164700
3548 2018699554

这里面sb和tb不同是由于StringBuilder没有重写hashCode方法,底层是使用Object类默认的hashCode()计算出来的对象存储地址,所以散列码自然也就不同了

HashMap重要概念
  • jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。

  • jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// key-value 模型
public class HashBucket {
private static class Node {
private int key;
private int value;
Node next;


public Node(int key, int value) {
this.key = key;
this.value = value;
}
}

private Node[] array;
private int size; // 当前的数据个数
private static final double LOAD_FACTOR = 0.75;
private static final int DEFAULT_SIZE = 8;//默认桶的大小

public int put(int key, int value) {
int index = key % array.length;
Node cur = array[index];
//遍历当前列表,看是否存在当前值
while (cur != null) {
if (cur.key == key) {
cur.value = value;
}
cur = cur.next;
}
//若无当前值,则进行头插法
Node node = new Node(key, value);
node.next = array[index];
array[index] = node;
size++;
//判断是否超载
if (loadFactor()>=LOAD_FACTOR){
//扩容
resize();
}
return 0;
}


private void resize() {
Node[] newArr=new Node[array.length*2];
for (int i = 0; i < array.length; i++) {
Node cur=array[i];
while(cur!=null){
//遍历链表,将数据储存到新数组
int newIndex=cur.key% newArr.length;
Node curN=cur.next;
cur.next=newArr[newIndex];
newArr[newIndex]=cur;
cur=curN;
}
}
array=newArr;
}


private double loadFactor() {
return size * 1.0 / array.length;
}


public HashBucket() {
array=new Node[10];
}


public int get(int key) {
int index=key%array.length;
Node cur=array[index];
while(cur!=null){
if (cur.key==key){
return cur.value;
}
cur=cur.next;
}
return -1;
}
}

1、hash函数计算数组下标:

HashMap中并不是用取模计算索引位置,而是用位运算
位运算的效率 > 取模

(1)h & length
h = 0001 0101 0111 0010 1110
l = 0000 0000 0000 0001 0000
这种情况得到的结果是要么0,要么16

(2)h & (length - 1)
h = 0001 0101 0111 0010 1110
l = 0000 0000 0000 0000 1111
这种情况得到的结果是散列随机的,0-15之间的随机值。从而减少hash碰撞。

2、扩容机制
(1)达到扩容条件后,则扩容为原数组长度的两倍
(2)将原来老的数据移入到新的数组中,在移植的过程中还要进行一次rehash()运算

image-20240303154846009

$$
LoadFactor={size \over capacity}
$$

思考:

HashSet的实现原理?

实现原理:底层基于HashMap,键保存数据,并且所有元素共享一个静态的、唯一的值对象作为映射的值。

注意:Hashmap的Key可以为NULL,但是只能保存一个key为NULL的值,如果继续插入将会覆盖

LinkedHashMap

原理和HashMap一样,但是LinkedHashMap通过一个双向链表保存了数据的顺序,该顺序可以是插入顺序或者是访问顺序

与HashMap不同的字段:

1
2
3
4
5
6
7
8
9
10
11
/**
* The head of the doubly linked list.
* 双向链表的头节点
*/
private transient Entry<K,V> header;
/**
* The iteration ordering method for this linked hash map: true
* for access-order, false for insertion-order.
* true表示最近最少使用次序,false表示插入顺序
*/
private final boolean accessOrder;

249993-20161215143544401-1850524627

注意:这里的header节点的Before保存的是链表最后一个元素,便于定位最后的元素

  1. put

新增元素的逻辑和HashMap相同,但是会把数据插入到链表的尾部,并且会根据removeEldestEntry方法判断是否淘汰最旧的元素(根据这个可以实现LRU)

注意:默认的构造器方法下removeEldestEntry默认为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void addEntry(int hash, K key, V value, int bucketIndex) {
//调用create方法,将新元素以双向链表的的形式加入到映射中
createEntry(hash, key, value, bucketIndex);


// removeEldestEntry:删除最近最少使用元素的策略定义
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
  1. get

查看逻辑与HashMap相同,但是会根据accessOrder来调整数据次序

注意:默认的构造器方法下accessOrder默认为false

  • accessOrdertrue时,会将元素移动到链表末尾
  • accessOrderfalse时,不操作
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 通过key获取value,与HashMap的区别是:当LinkedHashMap按访问顺序排序的时候,会将访问的当前节点移到链表尾部(头结点的前一个节点)
*/
public V get(Object key) {
// 调用父类HashMap的getEntry()方法,取得要查找的元素。
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
// 记录访问顺序。
e.recordAccess(this);
return e.value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 在HashMap的put和get方法中,会调用该方法,在HashMap中该方法为空
* 在LinkedHashMap中,当按访问顺序排序时,该方法会将当前节点插入到链表尾部(头结点的前一个节点),否则不做任何事
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//当LinkedHashMap按访问排序时
if (lm.accessOrder) {
lm.modCount++;
//移除当前节点
remove();
//将当前节点插入到头结点前面
addBefore(lm.header); // header的Before保存的是最后一个节点
}
}

遍历Map

1
2
3
4
5
6
7
8
9
10
11
12
//定义一个LinkedHashMap,并插入元素
LinkedHashMap<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
map.get("key1");
// 遍历LinkedHashMap
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> next = iterator.next();
System.out.println(next.getKey() + " : " + next.getValue());
}
1
2
3
4
// 输出
key2 : value2
key3 : value3
key1 : value1

实现一个LRU

1
2
3
4
5
6
7
8
9
10
11
public class LinkedHashMapLRU<K,V> extends LinkedHashMap<K,V> {
// 定义LRU缓存的长度
private final int capacity;
public LinkedHashMapLRU(int initialCapacity) {
super(initialCapacity, 0.75f, true);
this.capacity = initialCapacity;
}
public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}

TreeMap

基于红黑树实现,对Key可以进行排序操作,适合对键排序的场景

HashTable

线程安全

异常类

b553d3745bc99aad51446d148ab55ddb

unchecked和checked异常

  • unchecked:(RuntimeException)不要求开发者显式地处理或声明这些异常
  • checked:(继承Exception,非RuntimeException)要求代码中必须捕获处理
    • 其设计目的是保障部分异常的可预见性

Error&Exception

如何自定义异常?

自定义异常通常继承自Exception或者其子类如RuntimeException。实际上,是否继承Exception还是RuntimeException取决于是否希望该异常被捕获

Error类及其子类通常用来表示系统级错误或虚拟机自身的问题,这些问题不是应用程序应该处理的异常情况,因此自定义异常不应该继承Error

代码结构

代码块

代码化块又称为初始化块,属于类中的成员[即是类的一部分],类似于方法,将逻辑语句封装在方法体中,通过{}包围起来
但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。

1 、普通代码块:在方法或语句中出现的{}就称为普通代码块,类中的方法的方法体。

普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定–“先出现先执行”;

1
2
3
4
5
6
7
public class Student(){

public void getMoney(){
System.out.println("你好");
}

}

2、 构造代码块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块

构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数;

当子类继承了父类,子类创建对象,无论它的构造器是否已经被重写,他都会执行一遍父类的构造器方法,包括父类的构造器代码块

且执行父类的构造器代码块优先级 > 父类构造器方法

1
2
3
4
5
6
7
8
9
10
// 创建类时会执行
public class RunoobTest {
// 构造代码块
{
System.out.println("RunoobTest");
}
public static void main(String[] args) {
RunoobTest runoobTest = new RunoobTest();
}
}

3 、静态代码块:在java中使用static关键字声明的代码块

静态块用于初始化类,为类的属性初始化。每个静态代码块只会执行一次!!!由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行!!!

1
2
3
4
5
6
7
8
public class RunoobTest {
static {
System.out.println("RunoobTest");
}
public static void main(String[] args) {
RunoobTest runoobTest = new RunoobTest();
}
}

static代码块也叫静态代码块,作用就是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。如果是普通代码块,每创建一个对象,就执行。

类什么时候被加载[重要背!]

1.创建对象实例时(new);
2.创建子类对象实例,父类也会被加载,而且父类先被加载,子类后被加载
3.使用类的静态成员时(静态属性,静态方法) ,静态代码块先被执行。

执行优先级:静态代码块 > mian方法 > 构造代码块 > 构造方法

序列化和反序列化

序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。

序列化的时候系统将serialVersionUID写入到序列化的文件中去,当反序列化的时候系统会先去检测文件中的serialVersionUID是否跟当前的文件的serialVersionUID是否一致,如果一直反序列化不成功,就说明当前类跟序列化后的类发生了变化,比如是成员变量的数量或者是类型发生了变化,那么在反序列化时就会发生crash,并且回报出错误

Object对象输出流写入对象必须实现serializable接口

JRE/JDK/JVM是什么关系

  1. JDK:英文全称 Java Development Kit,是Java的开发工具包 JDK是提供给Java开发人员使用的,其中包含了Java的开发工具JRE。其中的开发工具包括:编译工具(javac.exe)打包工具(jar.exe)等。通俗的说就是开发用的

  2. JRE:英文全称 Java Runtime Environment,是Java运行环境 JRE包括Java虚拟机 (JVM Java Virtual Machine)和Java程序所需的核心类库等,如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。通俗的说就是运行用的

  3. JVM:英文全称 Java Virtual Machine,是java虚拟机。 它只认识.class为后缀的文件,它能将class文件中的字节码指令进行识别并调用操作系统向上的API完成动作。JVM是java能够跨平台的核心机制。通俗的说就是跨平台用的,就是把我们写的代码,转换成class文件用的。

抽象类和接口的区别

  • 抽象类的重点是对象(包括对象的属性,动作)

    有构造器,抽象类更像普通类的扩展,包含了一些抽象属性

  • 接口的侧重点是动作

  1. 方法默认public abstract修饰
  2. 变量默认public static final修饰

不同的是抽象类可以包含成员属性,每个类只能只能继承一个抽象类

什么是装箱?什么是拆箱?

装箱:基本类型转变为包装器类型的过程。
拆箱:包装器类型转变为基本类型的过程。

Java是一种完全面向对象的语言。因此,包括数字、字符、日期、布尔值等等在内的一切,都是对象。似乎只需要一种方式来对待这些对象就可以了。

阻塞队列

阻塞队列核心方法

方法类型 抛出异常 特殊值( 有返回值) 阻塞 超时
插入 add offer put offer
移除 remove poll take poll
判断队列首 element peek - -

函数式接口

顾名思义,是执行函数的接口,其目的就是为了执行一个方法

其设计的目的是以便使用Lambda表达式

在Java的接口中,你不需要使用abstract关键字来标记一个方法为抽象方法,因为接口中的所有方法默认就是抽象的

什么是函数式接口

函数式接口有且仅有一个抽象方法,可以有多个普通方法和静态方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface
public interface MyFunction {
int apply(int input); //唯一抽象方法!

default String apply1(){ //普通方法
return "input";
};

static String apply2(){ //静态方法
System.out.println("study");
return "study";
}
}
1
2
3
4
5
6
public static void main(String[] args)  {
MyFunction myFunction= (x) -> x*x;
System.out.println(myFunction.apply(10));
System.out.println(myFunction.apply1());
System.out.println(MyFunction.apply2());
}

示例

重写一个自己的函数式接口。在java中其实内置了很多已经写好的函数式接口,我们只需要重写他,就可以快速定义一个符合自己功能的函数接口,这里以Predict为例

1
2
3
4
@FunctionalInterface
public interface Predicate<T> { //提供一个值,最后返回一个布尔值
boolean test(T t);
}
  1. 定义自己的函数式接口
1
2
3
4
5
6
7
8
public class TestPredict implements Predicate<Integer> {
@Override
public boolean test(Integer integer) {
boolean b = integer > 2;
System.out.println(b);
return b;
}
}

以上式子等于

1
Predicate<Integer> predict = x -> x>2;
  1. 对集合进行使用
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
Predicate<Integer> predict = new TestPredict();
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.forEach(predict::test);
}

为什么浮点数运算的时候会有精度丢失的风险?

浮点数运算精度丢失代码演示:

1
2
3
4
5
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

为什么会出现这个问题呢?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

就比如说十进制下的 0.2 就没办法精确转换成二进制小数:

1
2
3
4
5
6
7
8
// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

枚举

视频中详细讲解了Java中的枚举概念,强调当对象有预定义且不变的值时,应使用枚举。作者通过实例展示了如何创建枚举类型,如定义四季。枚举对象不能通过new创建,而是直接取预设值。比较枚举时,推荐使用==,因为枚举的值和地址恒定不变。枚举默认继承自Enum,不可继承其他类型。枚举中可以设置属性,但作者建议不设置set方法以保持信息不可变,避免因一处修改影响全局的潜在bug

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
public enum Weather {
// Ctr + Shift + U 实现大写
SPRING(1),SUMMER(2),AUTUMN(3),WINTER(4);

private final Integer value;

Weather(Integer value) {
this.value = value;
}
}

EventListener监听接口详解

在Java中,实现EventListener接口的类通过注册到事件源上,从而能够监听和响应特定的事件。事件监听器机制在Java中通常用于图形用户界面(如Swing或AWT组件)以及在框架或库中(例如Spring框架)。下面是一个详细的步骤说明实现EventListener接口的类是如何进行事件监听的:

1. 定义事件监听器接口

首先,事件监听器接口定义了响应事件的方法。例如,在Swing中,ActionListener接口只有一个方法actionPerformed(ActionEvent e)

1
2
3
public interface ActionListener extends EventListener {
void actionPerformed(ActionEvent e);
}

2. 实现监听器接口

接下来,你需要创建一个类来实现EventListener接口。在这个类中,你必须实现接口中定义的所有方法。这些方法将在相应的事件发生时被调用。

1
2
3
4
5
6
public class MyActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Action performed!");
}
}

3. 注册监听器到事件源

然后,你需要将监听器实例注册到事件源上。事件源可以是任何可以生成事件的对象,如按钮、文本框等。在Swing中,你可以使用addActionListener方法来注册监听器。

1
2
JButton button = new JButton("Click me");
button.addActionListener(new MyActionListener());

在Spring框架中,你可以使用@EventListener注解来标记方法,使其能够监听特定的事件类型。

1
2
3
4
5
6
7
8
@Component
public class MySpringEventListener {

@EventListener
public void handleEvent(MyEvent event) {
System.out.println("Event received: " + event);
}
}

4. 触发事件

当事件源上的事件发生时,事件源会调用所有已注册监听器的相应方法。例如,当用户点击按钮时,actionPerformed方法将被调用。

5. 事件传播

在一些框架中,如Spring,事件可以在整个应用程序中传播,这意味着任何地方注册的监听器都有机会响应事件,只要它们实现了正确的EventListener接口。

总结

实现EventListener接口的Java类通过注册到事件源上,并实现接口中定义的方法来监听和响应事件。当事件发生时,事件源会调用监听器中对应的方法,从而触发相应的处理逻辑。这种方式使得Java应用程序能够以事件驱动的方式响应用户输入或系统事件,提高了程序的响应性和灵活性。

在Java中自定义事件通常涉及几个步骤:创建事件对象、定义事件监听器接口、实现事件监听器接口以及在事件源中触发事件。下面是详细的步骤和示例代码:

步骤1:创建事件对象

事件对象通常继承自java.util.EventObject类。这个类提供了事件的基本结构,包括事件的来源(事件源)。你可以根据需要添加额外的属性来携带事件的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.EventObject;

public class CustomEvent extends EventObject {
private Object data;

public CustomEvent(Object source, Object data) {
super(source);
this.data = data;
}

public Object getData() {
return data;
}
}

步骤2:定义事件监听器接口

事件监听器接口通常继承自java.util.EventListener接口,并定义一个或多个方法来响应事件。这些方法通常以onEventName的形式命名,其中EventName是事件的名称。

1
2
3
4
5
import java.util.EventListener;

public interface CustomEventListener extends EventListener {
void onCustomEvent(CustomEvent event);
}

步骤3:实现事件监听器

创建一个类来实现CustomEventListener接口,并在onCustomEvent方法中编写响应事件的逻辑。

1
2
3
4
5
6
public class CustomEventProcessor implements CustomEventListener {
@Override
public void onCustomEvent(CustomEvent event) {
System.out.println("Custom event received with data: " + event.getData());
}
}

步骤4:在事件源中触发事件

事件源是产生事件的对象。在事件源中,你需要维护一个事件监听器列表,并在事件发生时调用这些监听器的响应方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.List;

public class EventSource {
private List<CustomEventListener> listeners = new ArrayList<>();

public void addCustomEventListener(CustomEventListener listener) {
listeners.add(listener);
}

public void removeCustomEventListener(CustomEventListener listener) {
listeners.remove(listener);
}

public void triggerCustomEvent(Object data) {
CustomEvent event = new CustomEvent(this, data);
for (CustomEventListener listener : listeners) {
listener.onCustomEvent(event);
}
}
}

使用自定义事件

现在你可以创建事件源和监听器,然后将监听器添加到事件源,并触发事件。

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
EventSource source = new EventSource();
CustomEventProcessor processor = new CustomEventProcessor();
source.addCustomEventListener(processor);

// 触发事件
source.triggerCustomEvent("Hello, World!");
}
}

以上就是一个完整的自定义事件的实现流程。在实际应用中,你可能需要根据具体需求调整事件对象、监听器接口和事件源的实现细节。例如,在复杂的事件系统中,你可能需要支持多种不同类型的事件,或者需要更精细的事件过滤和处理机制。

八种基本数据类型

六种数字类型

四种整形,两种浮点型

整形:

byte、int、long、和short都可以用十进制、16进制以及8进制的方式来表示。

当使用字面量的时候,前缀 0 表示 8 进制,而前缀 0x 代表 16 进制, 例如:

1
2
3
int decimal = 100;
int octal = 0144;
int hexa = 0x64;
  • byte 8位(2^7)一个字节
  • short 16 位
  • int 32位
  • long 64位

浮点型

  • float 32位
  • double 64位

布尔型:boolean 一位

字符型:char 16位

  • 最小值是 \u0000(十进制等效值为 0);
  • 最大值是 \uffff(即为 65535);

Integer a1,a2=100;请问a1是否==a2

1
2
3
4
5
public static void main(String[] args) {
Integer a1 = 128;//默认调用Integer.valueOf()进行装箱,得到Intern对象
Integer a2 = 128;// 对象和对象虽然理论上地址不一样,但是其实Integer内部有缓存,在-127~128之间的值会返回同一个对象
System.out.println(a1 == a2); // true
}

Integer.valueOf(int)实现,里面包含了查询内存的细节

1
2
3
4
5
6
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

在Java中创建线程有两种主要方式

  1. 继承Thread类

    • 创建一个新的类,该类继承自Thread类。
    • 重写run()方法以定义线程要执行的任务。
    • 创建该类的实例,并调用其start()方法来启动线程。

    示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyThread extends Thread {
    public void run() {
    System.out.println("Thread is running...");
    }
    }

    public class Main {
    public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    }
    }
  2. 实现Runnable接口

    • 创建一个实现了Runnable接口的新类。
    • 实现run()方法以定义线程要执行的任务。
    • 创建该类的实例,并将其传递给Thread类的构造函数。
    • 调用Thread对象的start()方法来启动线程。

    示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyRunnable implements Runnable {
    public void run() {
    System.out.println("Thread is running...");
    }
    }

    public class Main {
    public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable());
    t.start();
    }
    }

另外,从Java 5开始,还可以使用CallableFuture接口以及ExecutorService来创建和管理线程。这种方式更灵活且功能更强大,可以处理有返回值的任务。

Java线程的状态

Java中的线程可能处于以下几种状态之一:

  1. NEW(新建):当一个线程被创建但还没有被启动时,它处于新建状态。
  2. RUNNABLE:线程正在运行或准备好运行。这包括正在执行的线程和等待CPU时间片的线程。
  3. BLOCKED(阻塞):通常指的是线程因为等待获取锁而无法运行的状态。
  4. WAITING(等待):线程进入了等待状态,等待其他线程的通知或唤醒。例如,通过调用Object.wait()Thread.join()Thread.sleep(long millis)等方法。
  5. TIMED_WAITING(定时等待):线程处于等待状态,但是有一个时间限制。例如,通过调用Thread.sleep(long millis)Object.wait(long timeout)等方法。
  6. TERMINATED(终止):线程已完成其任务或被异常终止。

这些状态由java.lang.Thread.State枚举类型表示,可以通过Thread.getState()方法获取线程的当前状态。需要注意的是,线程状态并不是绝对精确的,因为线程的状态可能会在调用状态检查方法和实际获得结果之间发生变化。

Java8新特性

  • Optional
  • Lambda表达式,函数式编程
  • Stream

Stream流

Java8新特性:StreamAPI(超详解)_stream api-CSDN博客

不同于集合,集合侧重于数据的保存,而流侧重于对数据的计算处理,他可以很好的结合函数式接口,从而达到简化代码的操作

  1. 创建Stream流
1
2
3
4
5
6
//数组
int[] arr = new int[]{1,2,3,4,5,6};
IntStream stream = Arrays.stream(arr);
//集合
List<Employee> employees = EmployeeData.getEmployees();
Stream<Employee> stream = employees.stream();
  1. 中间操作

filter

  • filter(Predicate p) 接收 Lambda, 从流中排除某些元素
  • distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
  • limit(long maxSize) 截断流,使其元素不超过给定数量
  • skip(long n) 跳过元素,返回一个扔掉了前 n个元素的流。若流中元素不足 n个,则返回一个空流。与 limit(n) 互补

map

  • map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
  • mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
  • mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
  • flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

sort

sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparator com) 产生一个新流,其中按比较器顺序排序
  1. 收集

collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//3-收集
@Test
public void test4(){
// collect(Collector c)——将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法
// 练习1:查找工资大于6000的员工,结果返回为一个List或Set

List<Employee> employees = EmployeeData.getEmployees();
List<Employee> employeeList = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());

employeeList.forEach(System.out::println);
System.out.println();
Set<Employee> employeeSet = employees.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toSet());

employeeSet.forEach(System.out::println);

}

java程序运行的几个阶段

泛型原理、优缺点

在编译的时候会把类型擦除,那泛型如何起作用呢,这是因为编译器在编译前会进行类型检查,类型不一致会直接编译报错

所以在编译之后我们使用反射机制动态的向集合插入不同类型的元素这是可以成立的

1
2
3
4
5
6
7
8
9
List<Long> list = new ArrayList<Long>();
list.add(100L);
list.getClass().getMethod("add", Object.class).invoke(list, "abc");


// 然后我们将list打印出来
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}

如果我们改成:

这个时候两段代码都会报出类型转换错误,一个在编译时检查的错误,一个是运行时类型转换的错误

1
2
3
4
String s = "";
Long a = 0L;
s = list.get(1); //编译前检查出错
a = list.get(1);// 运行时出错

优势:

  • 减少内存占用
  • 兼容1.5版本之前无泛型的版本

问题:

  • 不安全,因为仅仅在编译时期检测在运行时期可以跳过这个步骤,造成不稳定性
  • 不能使用基本数据类型,带来拆箱装箱损耗
  • 不能对方法重载
  • 静态方法无法引用类的泛型类型(Java中的泛型是类实例化的时候才能确定泛型的准确类型,而静态方法是不需要类实例化就能调用的,显然不能使用)

无论是是否用了擦除机制,在源码里面看到的都是取数据的时候都需要做强转

OOM

发生原因

线上解决方案

反射

定义:

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

核心:

反射就是把java类中的各种成分映射成一个个的Java对象

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象。通过这个Class对象可以动态获取到该类的属性,方法以及构造函数,通过构造函数可以进行反射生成对象。

示例:

  1. 反射生成对象
1
2
3
4
5
Constructor con = clazz.getConstructor(null);
//1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
//2>、返回的是描述这个无参构造函数的类对象。
//调用构造方法生成对象
Object obj = con.newInstance();
  1. 获取属性
1
2
3
4
5
6
7
8
9
Field f = stuClass.getField("name");
System.out.println(f);
//获取一个对象
Object obj = stuClass.getConstructor().newInstance();//产生Student对象--》Student stu = new Student();
//为字段设置值
f.set(obj, "刘德华");//为Student对象中的name属性赋值--》stu.name = "刘德华"
//验证
Student stu = (Student)obj;
System.out.println("验证姓名:" + stu.name);
  1. 调用方法
1
2
3
4
m = stuClass.getDeclaredMethod("show4", int.class);
System.out.println(m);
m.setAccessible(true);//解除私有限定
Object result = m.invoke(obj, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参

JavaWeb

Servlet

https://www.runoob.com/servlet/servlet-intro.html

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层

生命周期:

  • 加载:加载到web容器,一般为懒加载
  • 初始化:数据库连接等前置操作
  • 执行服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Servlet 生命周期方法:对客户端响应的方法,该方法会被执行多次,每次请求该servlet都会执行该方法
public void service(ServletRequest arg0, ServletResponse arg1)
throws ServletException, IOException {
System.out.println("hehe");

}
// HttpServlet则是实现doGet、doPost
public class ServletDemo3 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("haha");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("ee");
doGet(req,resp);
}

}
  • 销毁:Servlet 的 destroy() 方法。在这个方法中,可以释放资源,如关闭数据库连接等。调用 destroy() 方法后,Servlet 实例将被垃圾回收。

Forward和Redirect

都是做网页跳转

  • Forward:服务器内部做跳转,速度快,地址栏不会变化,且共享原请求的属性,可以传递数据
1
2
3
// 在Servlet中使用Forward
RequestDispatcher dispatcher = request.getRequestDispatcher("/targetPage.jsp");
dispatcher.forward(request, response);
  • Redirect:服务端返回一个状态码,然后浏览器再根据状态码跳转不同页面,不能传递数据
1
2
// 在Servlet中使用Redirect
response.sendRedirect("/contextPath/targetPage.jsp");

思考题

判断输出结果

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
// 输出 2.0 因为执行结果默认为int,然后(int)2 转型为 double
public static void main(String[] args){
double x = 10 /4;
System.out.println(x);
}

// 输出 2.5
public static void main(String[] args){
double x = (double)10/4;
System.out.println(x);
}

// 输出false
String s = "AD";
System.out.println(s.toLowerCase() == "ad");

// 编译不通过
/*
java: 二元运算符 '-' 的操作数类型错误
第一个类型: java.lang.String
第二个类型: int
*/
int a = 0; int b = 0;
System.out.println("a+b="+ a-b);

// 输出:finally、2
public static void main(String[] args) {
System.out.println(get());
}
private static int get() {
int a = 1;
try {
return a;
} finally {
System.out.println("finally!");
a++;
return a;
}
}

java类main方法打包成可执行jar

  1. 编写类
1
2
3
4
public class MatchCar {
public static void main(String[] args){
}
}
  1. 导入maven插件,并配置jar程序入口类
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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest> <!--指定入口类-->
<mainClass>org.fansea.MatchCar</mainClass>
</manifest>
</archive>
<descriptorRefs> <!--携带依赖的jar的后缀-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions> <!--在执行package动作的时候,自动打包-->
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
  1. 执行mavenpackage命令
1
maven clean package

image-20241101221958276

如果报错,将pom.xmljdk版本更改为1.8

1
2
3
4
5
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>