java笔记记录

本文最后更新于:2022年6月19日 下午

Java题目笔记

2022-06-19更新

Mysql(版本8.0.25)不支持full join, 执行full join 语句会报错。

2022-06-17更新

Java权限修饰符:

图片
private 和 public很容易理解,分别是只能类内部访问和任何地方都可以访问。
比较让人容易分不清楚的,默认default的和protected的,默认的权限是类内部和在同一个包下可以访问;保护的权限是类内部、同一个包下和不在同一个包中的子类中可以访问。

hashCode()和equals()

hashCode()方法并不完全可靠,有时不同对象的hashCode()生成的hash值也是相同的,所以这时候需要再使用equals()来进行比较。

那为什么不直接都使用equals()进行比较呢?因为重写的equals()方法往往比较复杂,为了提高程序的效率,一般先使用hashCode()进行比较,如果两个对象的hash值不相等,则两个对象肯定不相等,直接返回false;如果hash值相等,则需要再使用equals()进行比较。

所以,做个总结:

  1. equals()相等的两个对象,它们的hashCode()肯定相等,使用equals()对比是绝对可靠的。
  2. hashCode()相等的两个对象,它们的equals()不一定相等。hashCode()不是绝对可靠的。

正因为hashCode()不是绝对可靠的,所以需要再使用equals()来进行对比。

关于try、catch、finally的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
System.out.println(test());
}
private static int test() {
int temp = 1;
try {
System.out.println(temp); // 打印1
return ++temp; // temp:2, 保存这个值 2
} catch (Exception e) {
System.out.println(temp);
return ++temp;
} finally {
++temp; //执行finally中的该语句,--> temp:3
System.out.println(temp); // 打印3
} // 执行完finally中的语句,打印完3后,再返回到try中的return语句,返回当时保存的值 2
}
}

最终的顺序是:

  1. 先打印try中的1,temp加1变为2,保存当前这个值2
  2. 转去执行finally中的语句,temp加1变为3,打印3
  3. 执行完finally后,返回到try中的return语句,返回第1步保存的值2.
    所以最终的打印顺序是:1, 3, 2

try中的return的值会被保存到临时空间中,执行完finally后,方法结束,再返回临时空间中保存的值;
如果finally中有return temp+10;的话,则会刷新临时空间的值,最后仍返回临时空间的值。


类的私有变量,本类的方法可以访问,通过反射也可以访问到。

abstract和final可以同时作为一个类的修饰符吗?

abstract表示该类是一个抽象类。抽象类本身不能实例化,必须需要有子类继承它并实现抽象类中的所有抽象方法,子类才能实例化。
而final表示该类不能被继承。
因此这两个关键字的作用:前者表示该类必须被继承,后者表示该类不能被继承,是相克的,不能同时使用。

当把来自客户机的HTTP请求委托给servlet时,会调用HttpServlet的(service)方法

HttpServlet容器响应Web客户请求流程如下:
1)Web客户向Servlet容器发出Http请求;

2)Servlet容器解析Web客户的Http请求;

3)Servlet容器创建一个HttpRequest对象,在这个对象中封装Http请求信息;

4)Servlet容器创建一个HttpResponse对象;

5)Servlet容器调用HttpServlet的service方法,这个方法中会根据request的Method来判断具体是执行doGet还是doPost,把HttpRequest和HttpResponse对象作为service方法的参数传给HttpServlet对象;

6)HttpServlet调用HttpRequest的有关方法,获取HTTP请求信息;

7)HttpServlet调用HttpResponse的有关方法,生成响应数据;

8)Servlet容器把HttpServlet的响应结果传给Web客户。

doGet() 或 doPost() 是创建HttpServlet时需要覆盖的方法.

抽象类和接口不能实例化

抽象类 a = new 子类(); 是可以的。

字符流和字节流

如图
图片
字节流:InputStream, OutputStream

字符流:多用于处理字符串和文本。面向字符的输入流类都是Reader的子类,面向字符的输出流类都是Writer的子类。一般以 reader 或 writer 结尾。

JVM的内存结构

JVM图片

  • PC寄存器
  • 虚拟机栈
  • 方法区 (常量池)
  • 本地方法栈

2022-06-18

垃圾回收不能确定具体的回收时间

GC是完全自动的,不能被强制执行。
将某个局部变量置为null,只是表示可能会被回收,但是后面可能还会使用。

静态代码块、构造代码块和构造方法的执行时期

1.静态代码块 2.构造代码块3.构造方法的执行顺序是1>2>3;明白他们是干嘛的就理解了。
1.静态代码块:是在类的加载过程的第三步初始化的时候进行的,主要目的是给类变量赋予初始值。
2.构造代码块:是独立的,必须依附载体才能运行,Java会把构造代码块放到每种构造方法的前面,用于实例化一些共有的实例变量,减少代码量。
3.构造方法:用于实例化变量。
1是类级别的,2、3是实例级别的,自然1要优先23.
在就明白一点:对子类得主动使用会导致对其父类得主动使用,所以尽管实例化的是子类,但也会导致父类的初始化和实例化,且优于子类执行。

Java程序初始化工作可以在许多不同的代码块中来完成,它们的执行顺序如下:
父类的静态变量、父类的静态代码块、子类的静态变量、子类的静态代码块、
父类的非静态变量、父类的非静态代码块、父类的构造函数、
子类的非静态变量、子类的非静态代码块、子类的构造函数。

Java构造方法不能被static、final、synchronized、abstract、native修饰,但可以被public、private、protected修饰;

识别合法的构造方法;
1:构造方法可以被重载,一个构造方法可以通过this关键字调用另一个构造方法,this语句必须位于构造方法的第一行;
重载:方法的重载(overload):重载构成的条件:方法的名称相同,但参数类型或参数个数不同,才能构成方法的重载。

2 当一个类中没有定义任何构造方法,Java将自动提供一个缺省构造方法;
3 子类通过super关键字调用父类的一个构造方法;
4 当子类的某个构造方法没有通过super关键字调用父类的构造方法,通过这个构造方法创建子类对象时,会自动先调用父类的缺省构造方法
5 构造方法不能被static、final、synchronized、abstract、native修饰,但可以被public、private、protected修饰;
6 构造方法不是类的成员方法;
7 构造方法不能被继承。

8 java构造方法中的this关键字: 构造器的this指向同一个类中,用于调用同一个类中不同参数列表的另外一个构造器,必须放在第一行,否则会引起编译错误!
9 java构造方法中的super关键字:构造方法的super关键字用于调用其父类的构造方法,子类默认调用父类的构造方法,也就是说super()是默认调用的,显示调用必须放在构造方法第一行!

基本数据类型

整数型变量:byte, short, int ,long
浮点型:float, double
逻辑型:boolean
字符型:char

静态方法中不能直接调用非静态方法和非静态变量

会在编译时报错:
Non-static field ‘a’ cannot be referenced from a static context

1
2
3
4
5
6
7
8
public class Test1 {
// 非静态变量
int a[] = new int[6];
// 静态方法
public static void main(String[] args) {
System.out.println(a[0]); // 报错
}
}

Java的反射

  • 反射涉及的类如:Class在lang包下,Method, Filed 在java.lang.reflet包下。
  • 通过反射可以动态的实现一个接口,形成一个新的类,并可以用这个类创建对象,调用对象方法。即通过反射实现动态代理。
  • 通过反射,可以突破Java语言提供的对象成员、类成员的保护机制,访问一般方式不能访问的成员。例如:通过反射访问私有成员时,Field调用setAccessible可解除访问符限制。
  • 反射不能实现对字节码的修改
  • Java的反射机制会带来效率问题,使用cache和禁止安全检查等都可以提升反射的效率,但即使再怎么优化也不可能达到和直接调用类一样的效率,因为无论是通过字符串获取Class、Method还是Field,都需要JVM的动态链接机制动态的进行解析和匹配(即告诉JVM该如何去找这个类),而直接调用则不必。

8、面向对象

this

  • this修饰成员属性
  • this修饰成员函数
  • this可以在同一个类中调用构造器,this调用构造器必须放在第一行。

static

  • 修饰属性
  • 修饰方法:类的静态方法中不能访问非静态方法和非静态属性。
  • 修饰代码块:执行顺序,静态块–》构造块–》构造器–》成员方法中的普通块。注意:静态块只在类加载的时候执行一次。静态块中只能访问静态成员变量和静态成员方法,不能访问非静态成员方法和非静态成员方法。
  • 修饰内部类:

三大特性

封装

提高代码的安全性

继承

提高代码的复用性

父类private修饰的内容,子类实际上也继承,只是因为封装的特性阻碍了直接调用,但是提供了间接调用的方式,可以间接调用。

内存分析

权限修饰符

总结:
属性,方法:修饰符:四种:private,缺省,protected,public
类:修饰符:两种:缺省,public

以后写代码
一般属性:用private修饰 ,方法:用public修饰

重载和重写的区别:

重载:在同一个类中,当方法名相同,形参列表不同的时候 多个方法构成了重载
重写:在不同的类中,子类对父类提供的方法不满意的时候,要对父类的方法进行重写。

super

Object类

toString方法

equals方法

多态

提高代码的扩展性

final

修饰基本数据类型,变量值不能改变,是一个常量。

修饰引用数据类型,对象的引用,也就是对象的指向不能改变,对象的属性可以修改。

修饰方法,该方法不能被该类的子类重写。

修饰类,表示该类不能被继承,也就是没有子类。

抽象类,抽象方法

  • 抽象类是否可以创建对象?

不可以

  • 抽象类中是否有构造器?

有的。子类创建对象初始化时调用子类的构造器,会先使用super调用抽象类的构造器。

  • 抽象类是否可以被final修饰?

不能,因为抽象类就是用于被继承的,final修饰的类不能被继承,就没有子类了。

需要被子类重写的方法加上abstract,去除函数体,就是抽象方法了,抽象方法必须要被子类重写。

子类如果没有重写父类的全部抽象方法,那么子类也可以变成一个抽象类。

接口

抽象类与接口的区别

抽象类 接口
使用abstract修饰 使用interface修饰
不能实例化 不能实例化
有构造方法 无构造方法
含有抽象方法的类是抽象类,必须使用abstract修饰 一个类只能继承一个类,但是可以实现多个接口
可以含有抽象方法,也可以不包含抽象方法,抽象类中可以有具体的方法 接口中的方法均为抽象方法
若子类实现了抽象类的所有抽象方法,则子类不是抽象类;否则子类是抽象类 接口中不能包含实例域或静态方法(静态方法必须实现,而接口中都是抽象方法,不能实现)

Package ‘com.xxx.demo12’ clashes with class of same name

爆红:包名与类名冲突

JDK1.8前后接口的变化

JDK1.8前,接口仅包含常量和抽象方法 JDK1.8后,接口新增非抽象方法(可以有函数体)
常量:public static final 常量:public static final
抽象方法:public abstract 抽象方法:public abstract
非抽象方法:public default
实现类要想重写非抽象方法,那么default修饰符不能加
静态方法:public static
static不能省略;静态方法不能在实现类重写

为什么要在接口中加入非抽象方法?

Java 8新增了default方法,它可以在接口添加新功能特性,而且还不影响接口的实现类。

Q:在面向接口编程的过程中,如果发现原有的接口中,都需要添加一个相同的方法,有两种实现方案:

  1. 把接口换成抽象类,在抽象类中添加该方法(需要改动代码)
  2. 在接口中添加该抽象方法,在每一个接口的实现类中,都要添加相同的实现方法,代码的改动是相当大的。

两种方法对代码的改动都是很大的。如果使用接口的默认方法,是接口本身就拥有某些功能的实现,就很好的解决了问题。

内部类

成员内部类:静态成员内部类,非静态成员内部类

局部内部类:(方法中,块中,构造器中)

匿名内部类

9、异常

Error:错误

Exception:

  1. 检查异常
  2. 运行时异常

10、常用类

包装类

自动装箱,自动拆箱

日期类

Math类

Random类

String类

不可变的,底层实现是final char value[]也是数组,不过是final的。

StringBuider类,StringBuffer类

StringBuider的底层实现是数组char value = new char[capacity];,可变数组,可以动态扩容,最初数组容量为16,一旦向里面加入的字符串的长度大于16时,就会扩容,扩容规则为

1
2
3
4
5
6
7
8
9
10
11
12
13
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
调用下面的方法:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;// 扩容逻辑
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}

StringBuider类,StringBuffer类的区别?

底层都是可变char数组,区别是StringBuilder是线程不安全的,效率高;StringBuffer是线程安全的,效率低。

StringBuffer的大多数方法都加了synchronized关键字,上了锁,所以效率低,一般优先考虑使用StringBuilder。

11、集合

1、Collection接口

——List接口:实现类有,ArrayList,LinkedList

——Set接口:实现类有, HashSet, TreeSet

ArrayList实现类:

在jdk1.7中,数组初始默认容量为10,

数组的扩容;

1
int newCapacity = oldCapacity + (oldCapacity >> 1);

扩容1.5倍。原容量为10,新容量为15;

在jdk1.8中,调用空构造器,底层数组初始化为{},数组容量最初为0,然后变为10;

1
2
3
4
5
// jdk1.8
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

Vector容器,底层扩容数组长度为原来2倍。

1
2
3
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? // capacityIncrement=0
capacityIncrement : oldCapacity);
// int newCapacity = oldCapacity + oldCapacity

泛型

元素的类型参数叫做泛型

2、Map接口

HashMap原理及内部存储结构

https://juejin.cn/post/6844903763715555336#comment

HashMap源码线程安全性问题(1.8)

1 多线程的map.put()方法可能导致元素的丢失:

实验代码:

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
public class ConcurrentIssueDemo1 {

private static Map<String, String> map = new HashMap<>();

public static void main(String[] args) {
// 线程1 => t1
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 99999999; i++) {
map.put("thread1_key" + i, "thread1_value" + i);
}
}
}).start();
// 线程2 => t2
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 99999999; i++) {
map.put("thread2_key" + i, "thread2_value" + i);
}
}
}).start();
}
}

触发此问题的场景:

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
static final int TREEIFY_THRESHOLD = 8; // 树化阈值=8
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 初始化hash表tab,定义一个p,Node类型的
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;

// 通过hash值计算在hash表tab中的位置,并将这个位置上的元素赋值给p,如果该位置上为null,
// 则new一个新的node放在该位置。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果hash表中已经存在元素,就向该元素后面追加链表
Node<K,V> e; K k;
if (p.hash == hash && // 判断是否和已存在的元素相同
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
// 新建节点,并追加到链表
if ((e = p.next) == null) { // #1,在此处t1和t2两个线程同时执行完
p.next = newNode(hash, key, value, null); // #2
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

两个线程t1和t2同时执行到上述代码的注释#1处,然后一起向下执行,

t1线程执行map.put(“key2”, “value2”),t2线程执行map.put(“key3”, “value3”),

假设t1先执行p.next = newNode(hash, key, value, null),那么会新建节点key2,并追加链表;

然后t2也执行p.next = newNode(hash, key, value, null), 就会将p.next重新指向一个新的节点key3,

p.next原来指向的节点key2就丢失了。

这样就产生了线程安全问题

2 put和get并发时,可能导致get为null

场景:

当线程1执行put方法时,如果元素个数超出threshold需要扩容导致rehash,

线程2此时执行get方法,就由可能导致get得到的值为null。

扩容resize()方法源码:

先计算出扩容后的容量和threshold,然后根据新容量创建新hash表,然后将旧hash表中的元素rehash到新的hash表中。

重点在于创建新hash表和hash表赋值的两句代码。

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
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 上面就是得到 新的容量newCap 和 threshold newThr

@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // #1
table = newTab; // #2
if (oldTab != null) {
...... // 将旧hash表中的元素rehash到新hash表中
}
}
return newTab;
}

在代码#1位置,使用新的容量newCap创建新hash表newTab,

然后在#2的位置将新hash表赋值给变量table。

赋值完后,table是空的!

如果此时,有线程使用get方法获取值,会得到null。

TreeMap

Collections工具类

Stack

Queue

Deque

并发容器

ConcurrentMap

多线程 并发

线程常见方法:

  • join方法:当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。
    注意:必须先start,再join才有效。

  • setDaemon(true)方法; 将子线程设置为主线程的伴随线程,主线程停止,子线程也停止执行

线程安全问题:

竞争资源引发的,加锁

1、同步代码块

2、同步方法

3、Lock

数据库

项目:

spring 原理

spring mvc 原理

项目名称:仿牛客论坛项目

技术选型:SpringBoot+MySQL+Redis +Kafka+Elasticsearch

项目描述:本项目采用微服务架构的思想,主要涉及模块有权限模块、核心模块、性能模块、通知模块、搜素模块和其他模块。完成了xx模块后台代码的编写,解决了帖子审核、评论、异常等功能的开发,从中学习到了xx技术栈等【这里可以挑选几个自己比较熟悉的模块写上去】


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!