java基础

it2024-11-04  7

面向对象和面向过程的区别?

面向过程:

优点:性能好。缺点:没有面向对象易维护,易复用,易扩展

面向对象:和面向过程正好相反

什么是java虚拟机?为什么java被称作”平台无关的编程语言”?

JVM是通过仿真模拟真实计算机功能来实现的,实质是执行字节码文件(.class)的进程。Java程序已经编译器编译成字节码文件后,就能够在虚拟机上执行。虚拟机将字节码解释成具体平台的机器指令执行,就能够实现跨平台运行。

JDK和JRE有什么区别?

JDK是java的开发工具包,包含了jre,同时还包含了编译java源码的编译器javac,还包含了一些开发和调试的工具,文档和demo等。Jre是java运行环境,包含了jvm虚拟机和一些基本的类库。总的来说jdk支持java的开发和运行,jre只支持java的运行。

Java支持的数据类型有哪些?什么是自动拆装箱?

Byte 1个字节 short 2字节 char 2字节 int 4字节 long 8字节 float 4字节 double 8字节 boolean1字节

Java的自动装箱就是将java的基本类型转换成其对应的对象,java的自动拆箱就是将java的基本类型的对象转换成基本类型,比如Integer转换成int。

int 和 Integer 有什么区别?

Integer是int的包装类,int则是java的一种基本类型

Integer变量必须实例化后才能使用

Integer实际是对象的引用,当new一个Integer时,会在java堆中创建一个对象,Integer实际是一个指向此对象的指针(及存放的是该对象在java堆的地址)

Integer默认为null,int默认为0

Integer j = 100;相当于Integer i = Integer.valueOf(100).-128~127之间时,会把-128到127之间的数据用数组缓存下来,下次再写Integer j = 100 就会直接从缓存中获取。所以Integer比较尽量中equals方法。

java 中的 Math.round(-1.5) 等于多少?

Math.round是四舍五入函数,这里等于-1,Math.round(1.5)等于2

swtich是否能作用在byte上,是否能作用在long上,是否能作用在String上?

switch(expr1)中,expr1是一个整数表达式。因此传递给switch 和 case 语句的参数应该是 int、 short、 char 或者byte。long,string 都不能作用于swtich(1.7版本之后switch语句支持string类型)。

char型变量中能不能存贮一个中文汉字?为什么?

能够定义成为一个中文的,因为java中以unicode编码,一个char占2个字节,所以放一个中文是没问题的

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

String不属于基础数据类型。String是final修饰的java类,是引用类型。

Java中操作字符串都有哪些类,他们之间有什么区别?

String类:String的值是不可变的,这就导致每次对String进行修改操作都会产生新对象,效率低下且浪费空间。字符数组是final修饰的,不可修改。

StringBuffer类。StringBuffer是可变类,对字符串进行修改操作不会产生新的对象。StringBuffer对象有一定的缓冲区容量,当字符串大小没有超过容量时,不会重新分配新的容量,超过就会自动增加容量。它的线程是安全的,可以在多线程环境下使用。它的字符串操作方法都是synchronized修饰的,所以线程是安全的。

StringBuilder类。可变类,速度更快,线程不安全。

String str=”i”与String str = new String(“i”)一样吗?

不一样。”i”是字面量,在java程序编译成字节码文件时会被存储在class文件的常量池中(即静态常量池)。在类加载后,会进入方法区的运行时常量池中存放。(1.7以前)1.7之后字符串常量池被转移到了java堆中,在堆中创建字符串对象,字符串常量池中存放的是字符串的引用。New String(“i”)则直接在堆上又创建了一个字符串对象。两者内存地址不一样。

String s = new String(“xyz”);创建了几个String Object?

两个,一个是在堆中创建的s一个是在字符串常量池中创建的“xyz”。

如何将字符串反转?

使用 StringBuilder 或 StringBuffer 的 reverse 方法

 

String类的常用方法都有哪些?

Length,charAt,substring,compareTo,indexof(str)

toLowerCase(),toUpperCase,trim()两端去掉空格

Split分割字符串,valueOf基本类型转换字符串,replace替换字符串

String.Intern()放入字符串常量池中。

String.split(“”)在处理.或者|时需要加上\\,即String.split(“\\.”)或者String.split(“”)

 

String类是否可以被继承?

String类是final关键字修饰的,不可以被继承

 

== 和 equals 的区别是什么?

==是对内存地址(引用)进行比较,object的==和equals没有区别,但是String,和基本类型的equals重写了object的equals方法,改为了对值进行比较。

 

HashCode的原理,两个对象hashcode相同,equals()也一定相同吗?

在没有重写equals方法的情况下,equals相同则hashcode一定相同,反之则不一定。

在jdk中,object的hashcode方法是本地方法,也就是用c或c++实现的,该方法直接返回对象的内存地址。String的hashcode是char数组的数字每次乘31再叠加最后返回,因此每个不同的字符串返回的hashcode也是不一样的。使用31是因为性能好31 * i ==(i << 5) - i,计算可以用位运算替代,且不容易溢出。

在集合查找时,hashcode能大大降低比较次数,提高查找效率

java中对象规定,相同的对象必须具有相等的哈希码,如果两个对象的hashcode相同,他们并不一定相同。

 

&和&&的区别。

&跟&&都可以作逻辑与运算符,但是&&具有短路功能,当前面的逻辑为false时,后面不需要计算。&还可以用作位运算符。

 

final, finally, finalize的区别。

1.final修饰变量:

final修饰基本数据类型的变量时,必须赋初值且值无法改变。Final修饰引用变量时,该引用变量无法指向其他变量。

2.final修饰方法:方法不可以被子类重写。

3.final修饰类:类不可以被继承

finally关键字是try catch finally中的,一般情况下,不管是否发生异常都会执行。

一个对象的finalize()方法只会被调用一次,当对象需要回收的时候被调用.而且finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。

 

try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后?

会执行,在return前执行(finally中程序一定会被执行,return结束后程序结束,所以肯定在之前执行)。System.exit(0),则finally不会执行.

四种情况不会执行:

在finally语句块中发生异常。在前面的代码中用了System.exit()退出程序。程序所在的线程死亡。关闭cpu。

 

final关键字在java中有什么作用?

1.final修饰变量:

final修饰基本数据类型的变量时,必须赋初值且值无法改变。Final修饰引用变量时,该引用变量无法指向其他变量。

2.final修饰方法:方法不可以被子类重写。

3.final修饰类:类不可以被继承

 

Static关键字在java中有什么作用?

Static是一个修饰符,修饰成员(成员变量,成员函数)

Static修饰的成员被所有的对象共享.

Static优先对象存在,因为static的成员随着类的加载就已经存在.

static修饰的成员多了一种调用方式,可以直接被类名所调用,(类名.静态成员)。

static修饰的数据是共享数据,对象中的存储的是特有的数据。

静态成员或者代码块只会在类加载的时候初始化一遍.非静态成员或者静态代码块每次实例化对象都会初始化一次,然后再执行构造函数.对于继承关系,则优先完成父类的加载和初始化.

 

“static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?

Static是静态的意思,修饰成员(成员函数和成员变量)java中不能覆盖一个private或者static方法.private的方法不能被继承,所以无法override.static修饰的方法是静态绑定的,而override方法是为了实现多态,是动态绑定的,所以无法覆盖.

什么是多态?

一个类在不同的时候有不同的状态。

静态内部类和非静态内部类的区别?

是否可以在static环境中访问非static变量?

不行,非static环境可以访问static中的static变量,反之则不行.

 

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

抽象类可以没有抽象方法,但是有抽象方法的类一定是抽象类.

 

普通类和抽象类有哪些区别?

抽象类不能实例化

抽象类可以有抽象方法,抽象方法只需声明,无需实现.

含由抽象方法的类必须声明为抽象类.

抽象方法不能声明为静态(即不能用static修饰)

抽象方法不能用private修饰

抽象方法不能用final修饰

 

抽象类能使用final修饰吗?

不能,抽象类用于被继承,无法实例化,而final修饰类代表该类不可以被继承.

 

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

相同点:1.都不能被实例化。2.接口和抽象类都必须实现所有方法才能被实例化。

不同点:1.接口只能定义抽象方法不能有实现方法,抽象类可以。2.单继承,多实现。3.接口强调的是功能,抽象类强调的是所属关系。4.接口中的所有成员变量都是public static final静态不可修改,必须初始化,方法都是public abstract公开抽象的。抽象类跟普通类差不多。

抽象类的作用:1.对类型进行隐藏,可以构造出一组固定行为的抽象描述,一个行为可以有任意个可能的具体实现方式(即多态)。2.用于扩展对象的行为功能。

接口:1.比抽象类跟抽象,弥补java单继承的不足。2.降低耦合度。(依赖倒置原则)3.定义接口有利于代码的规范。(接口分离原则)

 

Overload和Override的区别。Overloaded的方法是否可以改变返回值的类型?

方法的重写Override和重载Overload是Java多态性的不同表现。重写Override是父类与子类之间多态性的一种表现。重载Overload是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,那么我们说该方法被重写了。子类的对象使用这个方法时,将调用子类中的定义。对子类而言,父类中的定义如同被“屏蔽”了一样。关于重载,如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,也就是参数签名不同,这种情况出现方法的重载。重载的方法是可以改变返回值的类型。

 

什么是值传递和引用传递?

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量.  引用传递一般是对于对象类型的变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。 所以对引用对象进行操作会同时改变原对象.  一般认为,java内的传递都是值传递.

 

Java支持多继承么?

Java不支持多继承,但可以实现多个接口。

 

Java中的异常处理机制的简单原理和应用。

异常指Java程序运行时(非编译)所发生的非正常情况或错误。

java对异常进行了分类,不同类型的异常使用了不同的java类,所有异常的根类为java.lang.Throwable.Throwable派生了2个子类:Error和Exception.

Error表示程序本身无法克服和恢复的一种严重错误,程序只有死的份,如内存溢出和死锁问题等系统问题。

Exception表示还能克服和恢复,其中又分为checked异常(除运行时以外的异常)和uncheck异常(运行时异常)。checked异常比如FileNotFoundException,运行时异常比如数组越界问题(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException),类转换异常(ClassCastException);

java为系统异常和普通异常提供了不同的解决方案,编译器强制checked异常必须try..catch处理或throws声明继续抛给上层调用方法处理。所以普通异常为checked异常,而系统异常可以处理也可以不处理。编译器不强制用try..catch或throws声明,所以系统异常成为uncheckde异常。

请写出你最常见到的5个runtime exception。 常见异常见:http://www.runoob.com/java/java-exceptions.html

5个RuntimeException:

NullPionterException

ArrayIndexOutOfBoundsException

StringIndexOutOfBoundsException

ClassCastException

NumberFormatException

JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗?

Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它是Throwable类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果系统会抛(throw)一个异常对象,可以通过它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try用来指定一块预防所有异常的程序;catch子句紧跟在try块后面,用来指定你想要捕获的异常的类型;throw语句用来明确地抛出一个异常;throws用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟);finally为确保一段代码不管发生什么异常状况都要被执行;try语句可以嵌套,每当遇到一个try语句,异常的结构就会被放入异常栈中,直到所有的try语句都完成。如果下一级的try语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的try语句或者最终将异常抛给JVM。

java中会存在内存泄漏吗,请简单描述。

内存泄漏是指无法释放已申请的内存空间。Java中有垃圾回收机制,一般对象不会发生内存泄漏,但是还是有发生内存泄漏的可能。主要原因是长生命周期对象持有短生命周期对象的引用就可能发生内存泄漏。

比如1.静态集合类所引用的对象不能被释放。2.监听器,释放对象的时候忘记去删除这些监听器可能增加内存泄漏的集合。3.数据库,网络和io等连接需要显示的调用close()方法将其连接关闭,否则不会被gc回收。4.单例模式,如果单例对象持有外部对象的引用,那么这个外部对象不能被jvm正常回收。

内存泄漏症状:

应用程序长时间连续运行时性能严重下降。应用程序中的OutOfMemoryError错误。应用程序崩溃。

内存泄漏类型:

1.static字段引起的内存泄漏。

Static字段修饰的变量(引用类型变量,而不是基本数据类型变量)的生命周期很长。

解决办法:最大限度的减少静态变量的使用,单例模式时,尽量使用懒加载的方式而不是立即加载的方式。

2.未关闭资源导致内存泄漏

每当创建连接或者打开流时,jvm都会为这些资源分配内存,如果连接或者流没有关闭,会持续占用内存,最终导致OOM

解决办法:finally块关闭资源,1.7后,可以使用try-with-resource块。

3.不正确的equals()和hashcode()

在hashmap和hashset这种集合中,常用到equals和hashcode,如果重写不合理,将会成为潜在的内存泄漏问题。

解决办法:正确重写equals和hashcode

4.Finalize方法造成的内存泄漏

重写finalize()方法,该类的对象不会立刻被垃圾收集器收集,如果finalize方法的代码有问题,那么会潜在的引发oom。

避免重写finalize()方法。

5.常量字符串造成的内存泄漏

一个很大的String对象,通过intern()手动放入字符串常量池中,只要程序运行,该字符串就会保留,这就会占用内存,造成内存泄漏。

解决办法:1.8之前,增加PermGen的大小,-XX:MaxPermSize=512m;升级Java版本,jdk1.7以后字符串常量池移动到了堆中。

6.使用ThreadLocal造成内存泄漏

使用threadLocal时,每个线程只要处于存在状态就可以保留对其ThreadLocal变量副本的隐式调用,且保留其自己的副本,使用不当,就会引起内存泄漏。

一旦线程不在,Threadlocals就应该被垃圾器收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾收集器回收。

解决方法:不在使用ThreadLocal时,调用remove方法。

 

Java中io流分为几种?

java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?

按照流的流向分,可以分为输入流和输出流;

按照操作单元划分,可以划分为字节流和字符流;

按照流的角色划分为节点流和处理流。

 

 

BIO,NIO和AIO

BIO:同步阻塞的io通信模型(一个连接一个线程),服务器端有一个Adapter线程会循环的调用accept()方法来接收客户端的读写请求。每当有一个连接过来,会分配一个线程来建立套接字,然后在这个套接字上进行读写操作。(缺点:线程资源宝贵,计算机系统资源有限,连接数有限)

伪异步的io通信模型:在BIO的基础上使用线程池技术,将Socket封装成一个task加入到消息队列当中,线程池维护一个消息队列和n个线程,然后对消息队列中的任务进行处理。因为线程池中的消息队列大小和线程池的数量可控,所以能够处理大量的连接,而不会消耗完计算机的系统资源。

NIO:同步非阻塞的io通信模型(一个请求一个线程),主要有三个重要的组件,buffer数据的缓冲区,channel数据的传输通道,selector选择器,它是对操作系统底层函数,select,poll和epoll的封装,用来实现channel多路复用。Channel注册在selector上,服务器端有一个线程负责selector的轮询,如果一个channel发生读写请求,那么这个channel就处于就绪状态,它会被selector轮询出来,通过selectorkey就能够得到所有就绪状态的channel,从而进行后续的io操作。对于客户端来说,它需要不断的轮询stream来查看数据是否准备完毕。(缺点:epoll的空轮询会占用cpu的使用率)

AIO:异步非阻塞的io通信模型(一个有效请求一个线程)。应用程序注册在操作系统内核上,当操作系统发生io事件的时候,它会准备好数据并通知应用程序,触发响应的函数,进行后续的业务处理。

Select,poll和epoll

它们都是用来实现io多路复用的。

Select:使用了一种fd(文件描述符)_set的数据结构,实质就是一个Long类型的数组,数组中的每个元素都能与其打开的文件句柄(Socket句柄)建立联系。当调用select函数时,内核会根据io状态修改fd_set的内容,从而通知执行select函数的进程哪些文件(Socket)可以读写。

(问题:每次需要把fd集合从用户态拷贝到内核态,并且连接数有限制,32位系统最大1024,64位系统最大2048)

Poll:跟select类似,但是它采用pollfd数据结构来代替fd_set数据结构,底层使用的是链表,这样就可以不用限制连接的数量。

Epoll是对select和poll的增强版本,它无需遍历整个被监听的文件描述符集合,只需要遍历那些被内核io事件异步唤醒而加入到ReadyList中的文件描述符集合就行。

Files的常用方法都有哪些?

Files.exists():检测文件路径是否存在。

Files.createFile():创建文件。

Files.createDirectory():创建文件夹。

Files.delete():删除一个文件或目录。

Files.copy():复制文件。

Files.move():移动文件。

Files.size():查看文件个数。

Files.read():读取文件。

Files.write():写入文件。

 

获取用键盘输入常用的两种方法

方法1.通过Scanner

Scanner input = new Scanner(System.in);

String s = input.nextLine();

Input.close();

方法2.通过BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

String s = input.readLine();

 

finalize方法

1.Java的GC只负责内存相关的清理,其他资源的清理必须由程序员手动完成,不然会引起资源泄漏。

2.调用GC并不保证GC实际执行。

3.finalize抛出的未捕获异常只会导致该对象的finalize执行退出。

4.用户可以自己调用对象的finalize方法,但是这种调用是正常的方法调用,和对象的销毁过程无关。

5.jvm保证在一个对象所占用的内存被回收之前,如果它实现了finalize方法,则该方法一定会被调用。Object的默认finalize什么都不做,为了效率,GC可以认为其不存在。

作用:

字类覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。Finalize的调用具有不确定性,这点跟C++中的析构函数不是对应的。不建议用finalize方法完成“非内存资源的清理工作”,但建议用于1.清理本地对象。2.作为确保某些非内存资源(socket,文件等)释放的补充。

毛病:

对象必须覆盖finalize方法Finalize只会在对象内存回收前调用一次Finalize的调用具有不确定行,只保证方法会调用,不保证方法里的任务会被执行完。(对象还在自救过程中,就被杀死了。)

总结:

Finalize()方法没有什么用(在新版java中已被弃用)

 

对象复活以及finalize()方法:

即使在可达性算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”。要真正宣告一个对象的死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

  如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的引用队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

  finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那么基本上它就真的被回收了。

  总结:finalize()并不是必须要执行的,它只能执行一次或者0次。如果在finalize中建立对象关联,则当前对象可以复活一次。Finalizer线程不保证一定执行finalize方法,因为此线程的优先级很低,获得CPU资源有限;而且这样会避免finalize执行缓慢或者发生死循环,从而导致整个GC奔溃

java容器

java容器有哪些?

ArrayList和LinkList的区别?

 

ArrayList删除元素

1.通过iterator迭代器来删除(推荐)

 

 

2.通过list.remove()删除列表,除了倒数第二个元素不会除问题外,删除其他元素会抛出ConcurrentModificationException异常,主要是modCount 跟expectedModCount不一致导致的。iterator.next函数会比较modCount和expectedModCount是否相等,因为list.remove()或者list.add()后,modCount会加减,导致跟expectedModCount不一致,就会抛出ConcurrentModificationException异常。但是删除倒数第二个后,size会减1,iterator.hasNext时cursor=size,然后return false,结束迭代。

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

HashSet的实现原理是基于HashMap来实现的,就相当于是利用了HashMap的键。

对于HashSet中保存的对象,请注意正确重写其equals和hashcode方法,以保证放入对象的唯一性。

TreeSet元素有序,是基于TreeMap来实现的。

TreeMap底层原理是红黑树。

LinkedHashMap底层是HashMap+双向链表(记录顺序)

HashMap和Hashtable有什么区别?

Hashtable中的方法是用synchronized 修饰的,所以线程是安全的。

Hashmap初始容量是16,hashtable的初始容量是11,两者的填充因子都是0.75

Hashmap初始容量建议设置:

Jdk中,当我门指定初始化容量capacity时,jdk会帮我们选取第一个大于capacity的2的n次幂。

因为 hash = hashCode() hash % length = {0, length - 1}效率不高, hash & (length - 1)效率很高,但是length必须是2的n次幂

但是hashmap中存放的数据大于初始化容量*负载因子,就会自动扩容。那么比如我们想存入6个元素,jdk会帮我们选择初始化容量8,但是8*0.75=6,6个元素就会扩容,会多进行一个自动扩容,效率会降低,于是就有了阿里的建议。

HashMap扩容是当前容量翻倍,即capacity*2,hashtable扩容是capacity*2 + 1

HashMap 从链表变红黑树的阈值是8,红黑树变成链表的阈值是6.作者是根据计算概率来确定的,根据泊松分布,在负载因子0.75的情况下,单个hash槽内元素个数为8的概率小于百万分之一。

负载因子为啥是0.75?

这是根据时间和空间的权衡来得到的,负载因子大,虽然空间利用率上去了,但是时间效率降低了。负载因子小,时间效率增加了,但是空间利用率下来了。

红黑树和链表转换的阈值为8和6?

因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率小于百万分之6,用概率证明。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的互相转化,为了预防这种情况的发生,设置为6

还有一种说法根据时间复杂度来计算,红黑树查找的时间复杂度为logn,而链表的平均时间复杂度为O(n/2)

HashMap1.7的时候采用的是数组+链表的数据结构

1.7的时候扩容机制是当前存放新数据时已经存储的数据数量必须大于等于阈值(capacity*负载因子)并且当前加入的数据必须发生hash冲突。1.7的时候采用头插入的方式,那么在多线程的情况下可能会发生死锁。

线程A先执行,执行完Entry<K,V> next = e.next;这行代码后挂起,然后线程B完整的执行完整个扩容流程,链表会反转。接着线程A唤醒,继续之前的往下执行,当while循环执行3次后会形成环形链表

HashMap1.8的时候采用的是数组+链表+红黑树的数据结构

1.8的时候扩容机制是当前存放新数据时已经存储的数据数量必须大于等于阈值。当然在链表变红黑树的时候,链表的长度大于8且数组长度小于64的时候优先扩容而不是变成红黑树,即链表变成红黑树的条件是链表的长度大于8且数组长度大于等于64.

1.8版本的hashmap采用的是尾插入的方式,且扩容的时候会通过e.hash & oldCap == 0 将链表拆分为高低两个链表,低链表的位置不变,高链表的位置变成低链表的位置+oldCap,oldCap << 1;

public class MyHashMap { public static void main(String[] args) { MyHashMap hm = new MyHashMap(); hm.put(1, 1); hm.put(2, 2); System.out.println(hm.get(1)); // 1 hm.remove(1); System.out.println(hm.get(1)); // -1 } private final int N = 100000; // 静态数组长度100000 private Node[] arr; public MyHashMap() { arr = new Node[N]; } public void put(int key, int value) { int idx = hash(key); if (arr[idx] == null) { // 没有发生哈希碰撞 arr[idx] = new Node(-1, -1); // 虚拟头节点 arr[idx].next = new Node(key, value); // 实际头节点 } else { Node prev = arr[idx]; // 从虚拟头开始遍历 while (prev.next != null) { if (prev.next.key == key) { prev.next.value = value; // 直接覆盖value return; } prev = prev.next; } prev.next = new Node(key, value); // 没有键则插入节点 } } public int get(int key) { int idx = hash(key); if (arr[idx] != null) { Node cur = arr[idx].next; // 从实际头节点开始寻找 while (cur != null) { if (cur.key == key) { return cur.value; // 找到 } cur = cur.next; } } return -1; // 没有找到 } public void remove(int key) { int idx = hash(key); if (arr[idx] != null) { Node prev = arr[idx]; while (prev.next != null) { if (prev.next.key == key) { // 删除节点 Node delNode = prev.next; prev.next = delNode.next; delNode.next = null; return; } prev = prev.next; } } } // 哈希函数 private int hash(int key) { return key % N; } // 链表节点 private class Node { int key; int value; Node next; Node(int key, int value) { this.key = key; this.value = value; } } }

Comparable和Comparator区别?

简述HashMap的工作原理?

HashMap1.8之前(采用的是头插法)

底层采用的是数组+链表的方式

 

 

1.当插入数据的时候,根据key的hashcode和数组容量会计算出数组的下标位置,如果该位置没有存放数据,直接把数据存储到数组中。若数组有数据,即发生hash冲突,就通过key的equals方法判断该位置链表中Entry的key是否相同,若相同说明key已经有了,就把entry的value覆盖。否则创建Entry并加入到链表后面。

2.当插入的数据大于数组的容量*加载因子0.75(阈值)时,就需要对数组进行扩容。然后重新计算每个Entry的位置。

3.查询的时候利用key的hashcode和数组长度计算出下标位置,再利用key的equals方法去数组和链表中查找Entry,如果查找到则返回Entry的value,否则返回null。

HashMap1.8之后(采用尾插法)

数据结构变成了 数组+链表+红黑树。

即当链表长度太长,查询速度会很慢。首先会想到采用二叉排序树,但是二叉排序树也会存在问题,当插入1,2,3,4,5,6这样的情况时,效率跟链表相同。需要平衡一下二叉排序树,就是红黑树。链表转红黑树,当数组长度<64时,优先扩容,当大于等于64时才转红黑树,阈值=8,实际链表长度是9。

缺点:1.7版本Hashmap在多线程场景下线程不安全,在扩容的时候,链表会出现环,导致死锁。1.8版本优化了,不会出现死锁现象。利用了loHead,loTail(低位指针),hitail,hihead(高位)四个指针

利用 e代表value, e.hash& length == 0 ? 低位 :高位,从而拆分成2链表,lo放在对应的位置,hi放在 lo + length。

加载因子0.75在空间和时间上取得了一个均衡。

 

Iterator怎么使用?有什么特点?

ConcurrentHashMap的实现原理?

1.8之前

ConcurrentHashMap由数组+segment(分段锁)组成

ConcurrentHashMap使用分段锁(ReentrantLock)技术,将数据分成一段一段的存储,每段数据分配一把锁。当线程访问一段数据的时候,其他数据能够被访问,从而实现并发。Segment类似一个hashmap结构,内部有一个数组,数组中的每个元素对应一个链表。所以ConcurrentMap定位一个元素的过程需要两次hash操作。第一次hash定位到segment,第二次hash定位到元素所在的链表头部。

缺点:需要两次hash操作,hash的时间比hashmap久。

优点:使用分段锁技术,提高了并发量。

 

1.8之后

ConcurrentHashMap跟1.8的hashmap原理类似,采用数组+链表+红黑树的结构。

不同点是concurrnetHashMap大量采用了cas操作(乐观锁技术)

volatile保证并发的可见性和有序性。

Volatile配合cas来修改value和next指针,并且某些地方也使用了synchronized修饰代码块,所以锁的粒度比segment要小很多,segment是继承Reentrantlock来实现锁的

 

ArrayList和Vector的区别是什么?

 

Collection和Collections有什么区别?

TreeMap:红黑树的实现?

Collection.sort()原理

CAS实现原理,以及ABA问题

 

 

最新回复(0)