java学习

107 阅读39分钟

一、JAVA

1、JAVA基础(14)

①、 JDK 和 JRE 有什么区别?

JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境

②、== 和 equals 的区别是什么?

== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
        
String的equals被重写过,Object的equals比较内存地址,String的equals比较内容    

③、两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

不对,两个对象的 hashCode()相同,equals()不一定 true。
代码解读:很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。

④、final 在 java 中有什么作用?

final 修饰的类叫最终类,该类不能被继承。
final 修饰的方法不能被重写。
final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。

⑤、String 属于基础的数据类型吗?

String 不属于基础类型,基础类型有 8 种:bytebooleancharshortintfloatlongdouble,而 String 属于对象。

⑥、java 中操作字符串都有哪些类?它们之间有什么区别?

操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。

⑦、String str="i"与 String str=new String("i")一样吗?

不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String("i") 则会被分到堆内存中。

⑧、抽象类必须要有抽象方法吗?

不需要,抽象类不一定非要有抽象方法。
    
普通类不能包含抽象方法,抽象类可以包含抽象方法。
抽象类不能直接实例化,普通类可以直接实例化。

⑨、抽象类能使用 final 修饰吗?

不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类

⑩、接口和抽象类有什么区别?

实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。

⑪、 java 中 IO 流分为几种?

按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。

⑫ 、Collection 和 Collections 有什么区别?

java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

⑬、常用的容器

img

⑭ 、List、Set、Map 之间的区别是什么?

img

⑮、字符串拼接用“+” 还是 StringBuilder?

Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。

String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;

字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象
在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。

⑯ 、finally中的代码一定会执行吗?

不一定

1、走到finally之前虚拟机被停止

2、程序所在的线程死亡

3、关闭cpu

⑰ 、什么是序列化?什么是反序列化

  • 序列化: 将数据结构或对象转换成二进制字节流的过程(如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中)
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

⑱ 、什么是语法糖

代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响

例:

    for (String s : strs) {
    	System.out.println(s);
    }

⑲、泛型

public static void method(List<String> list) {
    System.out.println("invoke method(List<String> list)");
}

public static void method(List<Integer> list) {
    System.out.println("invoke method(List<Integer> list)");
}

编译会出错,因为编译后的结果会擦除泛型,也就是两个List

2、多线程(12)

①、并行和并发有什么区别?

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

②、 线程和进程的区别?

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

③、创建线程有哪几种方式?

1、继承Thread类创建线程类
2、通过Runnable接口创建线程类
3、通过Callable和Future创建线程

④、说一下 runnable 和 callable 有什么区别?

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

⑤、线程有哪些状态?

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
Java Api有6种常量:
NEW 尚未启动
RUNNABLE 正在执行中
BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
WAITING 永久等待状态
TIMED_WAITING 等待指定的时间重新被唤醒的状态
TERMINATED 执行完成

⑥、sleep() 和 wait() 有什么区别?

sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。

⑦、notify()和 notifyAll()有什么区别?

notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。

⑧、线程的 run()和 start()有什么区别?

start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

⑨、线程池中 submit()和 execute()方法有什么区别?

1、接收的参数不一样
2、submit有返回值,而execute没有
3、submit方便Exception处理

⑩、在 java 程序中怎么保证多线程的运行安全?

1、原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
2、可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
3、有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

⑪、说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象

synchronized 和 Lock 有什么区别?

首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

13、怎么查看当前线程的状态?

Thread.getState()
死亡后的线程不能再次启动后    

14、产生死锁的四个条件?

1、互斥条件。一个资源每次只能被一个进程使用。
2、请求与保持条件。一个进程因请求资源而阻塞时,对以获得的资源保持不放。
3、不剥夺条件。进程若已获得资源,在未使用完成之前,不能被剥夺。
4、循环等待条件。若干进程之间形成一种头尾相接循环等待资源关系。    

15、如何防止死锁?

1、尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
2、尽量使用 Java. util. concurrent 并发类代替自己手写锁。
3、尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
4、尽量减少同步的代码块。

16、Executor 和 Executors 的区别?

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。

Executor 接口对象能执行我们的线程任务。

ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

使用 ThreadPoolExecutor 可以创建自定义线程池。

Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果

3、堆、栈和常量池(3)

①、栈与堆(存在栈中的数据可以共享)

1、栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆

2、栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享

3、堆的优势是可以动态地分配内存大小,所有使用new xxx()构造出来的对象都在堆中存储,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢

②、常量池(存放字符串常量和基本类型常量)

1、常量池的好处是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。

例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

③、Java的两种数据类型

一、基本类型(int, short, long, byte, float, double, boolean, char)八种

int a = 3; 
int b = 3;

如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。 

存在栈中的数据可以共享

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况

1.png

如上例,我们定义完 a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值

2.png

二、包装类型

如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

String是一个特殊的包装类数据

即可以用

String str = new String("abc"); 的形式来创建(规范的类的创建过程。对象都new)

也可以用

String str = "abc"; 的形式来创建

此方式并不违反对象必须new的规则,因有如下步骤:
(1) 先定义一个名为str的对String类的对象引用变量放入栈中。 
(2) 在常量池中查找是否存在内容为"abc"字符串对象。
(3) 如果不存在则在常量池中创建"abc",并让str引用该对象。
(4) 如果存在则直接让str引用该对象。

举例(一):

String str1 = "abc"; 
String str2 = "abc"; 
System.out.println(str1==str2); //true

//结果创建了两个对象一个引用都指向常量池的 Str

3.png

举例(二):

String str1 = "abc"; 
String str2 = "abc"; 
str1 = "bcd"; 
System.out.println(str1 + "," + str2); //bcd, abc 
System.out.println(str1==str2); //false

//赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。

//事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。 

4.png

举例(三)

String str1 = "abc"; 
String str2 = "abc"; 

str1 = "bcd"; 

String str3 = str1; 
System.out.println(str3); //bcd 

String str4 = "bcd"; 
System.out.println(str1 == str4); //true 
    
 //str3这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用 str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

5.png 举例(三)

    String str1 = new String("abc"); 
    String str2 = "abc"; 
    System.out.println(str1==str2); //false 

    //创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象

6.jpeg

对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。

这也就是有道面试题:String s = new String(“abc”);产生几个对象?答:一个或两个,如果常量池中原来没有”abc”,就是两个。

4、IO

5、注解

①、什么是元注解,元注解有什么用?

Java定义了4个标准meta-annotate类型,他们用来提供对其他annotate类型做说明

②、元注解有几个?

4个分别是

@Taget:用于描述注解的使用范围

@Retention:表示在什么级别保存该注释信息,用于描述注解生命周期(SOURCE<CLASS<RUNTIME)

@Document:该注释将被包含在JavaDoc中

@Inherited:说明了子类可以继承父类中该注解

③、如何定义自定义注解?

使用@Interface(使用该注解就自动继承Annotation接口)关键字 + 4个元注解

举例:

public @Interface myAnnotation{

//定义参数名称格式 参数类型+参数名+()

String[] value ();

}

6、反射

①、类加载的过程?

分为三部:

第一:类的加载(Load),将Class文件加入内存,并创建一个java.lang.class对象,此过程由类加载器完成。

第二:类的连接(Link),将JAVA类的二进制代码合并到JVM的运行状态中的过程

(1)验证:确保加载的类信息符合JVM规范,没有安全方面问题。

(2)准备:正式为类变量(static)分配内存并设置变量默认初始值的阶段,这些内存都在方法区中进行分配。

(3)解析:虚拟机常量池内的符号引用(常量名)直接替换为直接引用(地址)的过程(引用类型替换为真实类型)

第三:类的初始化(Initialize),JVM负责对类进行初始化

(1)执行类构造器clinit的过程。所有static修饰的方法和代码块都会合并成一个

(2)初始化的时候发现父类没有初始化,先初始化父类。

(3)虚拟机保证一个类的clinit()方法在多线程被正确加锁和同步

②、什么时候发生类的初始化?

主动引用:一定会发生类初始化

1、虚拟机启动,首先会初始化main
2new一个类的对象
3、调用类的静态成员(除了final常量)和静态方法
4、反射也会调用
5、初始化类,如果父类没初始化,会先初始化父类

被动引用:不会发生类初始化

1、通过数组定义的类引用不会被初始化。
2、引用常量不会被初始化
3、访问一个静态域时,只有真正声明这个域的类才会被初始化(例:子类引用父类静态变量,子类不会被初始化)

③、有哪几种加载器?

三种:
    1、系统类加载器。用于加载 java-classpath || java.class.path所指目录下的Jar包装入工作
    2、扩展类加载器。用于加载jre/lib/ext目录下的Jar包 || java.ext.dirs指定目录下的Jar包装入工作
    3、引导类加载器。C++编写,JVM自带的类加载器。负责JAVA平台核心库。该加载器无法直接获取    

④、什么是双亲委派机制?

简答:自己手写一个java.lang.String不会被加载因为和JDk的String重名,用户加载器或让父类加载器先去寻找,父加载器有String,子加载器(用户加载器)就不会加载String

⑤、java有哪些反射类API

class、Field、Method、Constructor

⑥、反射创建对象步骤

1、通过反射获取对象(getClass() || Class.forName() || 对象.class || 包装类型.Type2、通过newInstance()   (需要有无参构造,并可以访问)     ||   通过构造方法创建getDeclaredConstructor()
注意:调用方法用到invok,私有方法无法访问可以关掉安全检查

7、java容器

8.png

①、List, Set, Queue, Map 四者的区别?

List(对付顺序的好帮手): 存储的元素是有序的、可重复的。

Set(注重独一无二的性质): 存储的元素不可重复的。

Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。

Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值

②、Collection 接口下面的集合

List

ArrayList 插入和删除元素的时间复杂度?

对于插入:

  • 头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O(n)。
  • 尾部插入:当 ArrayList 的容量未达到极限时,往列表末尾插入元素的时间复杂度是 O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次 O(n) 的操作将原数组复制到新的更大的数组中,然后再执行 O(1) 的操作添加元素。
  • 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置,然后再把新元素放入指定位置。这个过程需要移动平均 n/2 个元素,因此时间复杂度为 O(n)。

对于删除:

  • 头部删除:由于需要将所有元素依次向前移动一个位置,因此时间复杂度是 O(n)。
  • 尾部删除:当删除的元素位于列表末尾时,时间复杂度为 O(1)。
  • 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。

ArrayList 与 LinkedList 区别?

  • 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;

  • 底层数据结构: ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)

  • 插入和删除是否受元素位置的影响:

    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
    • LinkedList 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)addFirst(E e)addLast(E e)removeFirst()removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element)remove(Object o),remove(int index)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

  • 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)

ArrayList为什么扩容1.5倍?

grow(扩容方法中)

// 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,

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

所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)! 奇偶不同,比如:10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数.

  • VectorObject[] 数组。
  • LinkedList:双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。详细可以查看:LinkedList 源码分析

LinkedList 插入和删除元素的时间复杂度?

  • 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。
  • 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。

这里简单列举一个例子:假如我们要删除节点 9 的话,需要先遍历链表找到该节点。然后,再执行相应节点指针指向的更改

9.jpg 双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。

10.png

双向循环链表: 最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。

11.png

Set

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素。
  • LinkedHashSet: LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。

Queue

③、Map 接口下面的集合

HashMap

JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。详细可以查看:HashMap源码解析.java

默认的初始容量是16

//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//如果指定容量的话,HasMap会根据传的容量值计算 该值2的N次方幂 作为初始容量
HashMap<String,Object> mapLog = new HashMap<>(7);
//以上初始容量为 2×2×2 = 8;
//等到map元素个数达到 8 × 0.75 = 6 就开始对map进行扩容,所以这么设置容量不合理

//以下为设置合理值公式
initialCapacity = (需要存储的元素个数 / 负载因子) + 1

(扩容:resize()方法,超过最大值就不再扩充了,没超过就 newCap = oldCap << 1 :扩充为原来的2倍)对一个数左移1位就是乘以2,左移n位就是乘以2的n次方.右移n位就是除以2的n次方,当得到的商不是整数时会往小取整

loadFactor 负载因子

loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。

loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

LinkedHashMap

LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

Hashtable

数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的。

TreeMap

红黑树(自平衡的排序二叉树)。

ConcurrentHashMap

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

  • 实现线程安全的方式(重要):

    • 在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
    • CAS(CAS全称Compare and Swap,即比较并替换。CAS本质上很简单,一般至少有3个参数:一个变量v,旧值A,新值B。当且仅当变量v当前的值和旧值A相同时,才会将v的值更新为B。这整个操作是原子化的,不同平台的JVM也有不同的实现,一般以Native方法执行。在ConcurrentHashMap中,每个bucket都有一个预期的值(通常是头一个元素的值),当有新的元素需要插入时,会使用CAS操作来比较和替换当前元素的值。如果成功,则插入新元素;如果失败,则说明已经有其他线程修改过该元素,此时会重新尝试插入或者进行其他处理。)CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。 (java的juc包下有Atmoic开头的数据类型都是基于CAS的)
    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

    ConcurrentHashMap中的key和value可以为null吗

    不可以,因为源码中是这样判断的,进行put()操作的时候如果key为null或者value为null,会抛出NullPointerException空指针异常。为 NULL会存在二义性。

    这个二义性在非线程安全的HashMap中可以通过map.containsKey(key)方法来判断,如果返回true,说明key存在只是对应的value值为空。如果返回false,说明这个key没有在map中映射过。这样是为什么HashMap可以允许键值为null的原因,但是ConcurrentHashMap只用这个判断是判断不了二义性的。

    为什么ConcurrentHashMap判断不了呢

    此时如果有A、B两个线程,A线程调用ConcurrentHashMap.get(key)方法返回null,但是我们不知道这个null是因为key没有在map中映射还是本身存的value值就是null,此时我们假设有一个key没有在map中映射过,也就是map中不存在这个key,此时我们调用ConcurrentHashMap.containsKey(key)方法去做一个判断,我们期望的返回结果是false。但是恰好在A线程get(key)之后,调用constainsKey(key)方法之前B线程执行了ConcurrentHashMap.put(key,null),那么当A线程执行完containsKey(key)方法之后我们得到的结果是true,与我们预期的结果就不相符了。

    (ConcurrentHashMap的作者Doug Lea认为map中允许键值为null是一种不合理的设计,HashMap虽然可以判断二义性,但是Doug Lea仍然觉得这样设计是不合理的。)

ConcurrentHashMap的并发度是什么

程序在运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。

二、数据库

①、什么是存储过程?

存储过程是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。

②、左连接,右链接和内连接

#左连接是先查询出左表(即以左表为主),然后查询右表,右表中满足条件的显示出来,不满足条件的显示NULL。
select * from Menu1 m1 left join Menu2 m2 on m1.id = m2.id;
    
#右连接就是先把右表中所有记录都查询出来,然后左表满足条件的显示,不满足显示NULL。
select * from Menu1 m1 right join Menu2 m2 on m1.id = m2.id;
    
#内连接查询结果必须满足条件,返回同时满足两个表的部分。
select * from Menu1 m1 inner join Menu2 m2 on m1.id = m2.id;
    
注意:在 LEFT JOIN 中使用 WHERE 子句过滤可能会影响结果的行为,因为它实际上是在过滤连接后的结果集
而不是原始表。如果只想过滤与左表相关的行,可以考虑在 ON 子句中应用条件,而不是在 WHERE 子句中。
例:select * from Menu1 m1 left join Menu2 m2 on m1.id = m2.id AND m2.id = 1;

③、mysql的索引(很多)

1B+数索引
2、哈希索引

④、聚集索引和非聚集索引

  • 聚簇索引就是以主键创建的索引
  • 非聚簇索引就是以非主键创建的索引
  • 聚簇索引在叶子节点存储的是表中的数据
  • 非聚簇索引在叶子节点存储的是主键和索引列

⑤、MySQL中InnoDB和MyISAM的区别

1、InnoDB支持事务(每一条sql语言都默认封装成事务自动提交,影响速度),MySAM不支持事务

2、InnoDB支持外键,MyISAP不支持

3、InnoDB是聚集索引,MyISAM是非聚集索引(都是用B+数作为索引)

4、InnoDB支持表级锁、行级锁,MyISAM只支持表级锁

1、并发事务带来的问题

①、脏读?

一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。

例如:事务 1 读取某表中的数据 A=20,事务 1 修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20。

②、不可重复读

指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据

事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同。

③、幻读

幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。

事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据。

④、丢失修改

在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。

事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 先修改 A=A-1,事务 2 后来也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。

2、事务隔离级别

READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。

REPEATABLE-READ(可重复读 默认) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行

3 、并发事务的控制方式有哪些?

MySQL 中并发事务的控制方式无非就两种:MVCC

共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。

排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)

4、表锁和行锁

表锁针对索引字段,行锁针对非索引字段

执行 UPDATEDELETE 时WHERE字段没有命中唯一索引或者索引失效会导致扫描全表并对全表加锁。

5、MySql慢SQL问题查找并解决

explan 查询执行计划逐步分析

三、redis

①、redis有几种数据类型

5种基本:

  • String(字符串)
  • Hash(哈希)
  • List(列表)
  • Set(集合)
  • zset(有序集合)

3种特殊

  • Geospatial
  • Hyperloglog
  • Bitmap

②、缓存使用方式图

7转存失败,建议直接上传图片文件

③、缓存穿透

通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。

Ⅰ、产生原因

  • 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
  • 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
  • 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。

Ⅱ、解决方法

  • 1.如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
  • 2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有效请求进来的话,需要更新缓存,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
  • 3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。

四、Spring

①、Spring中Bean是线程安全的吗?

不是。Bean分为单例和多例。多例操作不同对象线程安全,单例又分为有状(态修改)和无状态(非修改),有状态情况下线程会不安全

解决:

1、不用单例

2、使用ThreadLocal(会隔离线程)

②、SpringBoot启动注解

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @ComponentScan:扫描被@Component (@Repository,@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
  • @Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类

RequestBodyAdvice和ResponseBodyAdvice详解,@ControllerAdvice注解(接口加密解密)

1、事务失效场景

①、访问权限不足

spring 要求被代理方法必须是public

②、方法用 final 修饰

spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能

③、方法内部调用

在同一个类中的方法直接内部调用,会导致事务失效(因为 spring aop 生成代理了对象,但是这种方法直接调用了 this 对象的方法)

④、未被 spring 管理

没有被Spring容器管理

⑤、表不支持事务

比如mysql的M有ISAM引擎

⑮⑯ ⑰ ⑱ ⑲ ⑳ ①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗ ㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿