206(1~27)道Java面试题
JDK:Java 开发工具包,Java的运行环境和开发环境。
JRE:Java 运行环境,为 Java 的运行提供了所需环境
JDK(JRE,javac–java源码的编译器,其他Java程序的调试和分析工具)。
如果只要运行Java只需要JRE,但是要编写Java程序就需要JDK。
当比较的是两个string对象时,str1==str2结果取决于string的创建方式。
当string是通过new关键字创建的时,str1,str2指向两个不同的对象,返回false。
当string是直接赋值时,由于在都是指向同一个字符串常量池的“hello”,返回true。
**equals**
equals是Object类的一个方法,用于比较两个对象的内容是否相同。
默认下,equals方法与==在引用类型上的行为相同,都是比较内容地址。但是string类型数据时,不管string数据是否通过new创建,比较的都是内容。因为string类重写了equals()方法.
大多数情况下都重写了equals()方法,以便比较对象的内容而不是地址。
如果创建了自定义类,并且希望比较对象的内容而不是内存地址,那么需要重写 equals()
方法。同时,通常还需要重写 hashCode()
方法,以保持 equals()
和 hashCode()
的一致性.
不一定
如果两个对象的equals()值相同,那么对象的hashCode()值一定相同。
因为在散列表中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
哈希值相等:两个对象的hashCode()方法返回相同的整数值,是确定对象存储位置的基础。
键值对相同:键值对相等通常意味着两个键值对的键和值都相等。对于键来说,这通常是通过调用equals()
方法来确定的。
哈希值相等不一定意味着键值对相等:尽管hashCode()值相同(映射到了散列表的同一个桶),但是可能产生哈希冲突,即不同对象被映射到相同的哈希值。在这种情况下,散列表需要使用其他机制(如链表、红黑树等)来存储这些具有相同哈希值但不相等的对象,并在需要时通过equals()
方法进行比较以确定它们是否真正相等。当发生哈希冲突时,散列表必须使用其他机制来确保能够准确地定位和比较对象。
-
4.final在Java中有什么作用?
-
final修饰的类不能被继承。
-
final修饰的方法不能在任何子类中被重写。
-
final修饰的成员变量(变量实例或类变量)的值不能在初始化后改变。
-
final修饰的局部变量必须在使用前初始化,并且一旦初始化后值不能改变。
-
-
5.Java中的 Math.round(-1.5)等于多少?
Math.round(x)的使用方法:
小数点后第一位<5
正数:Math.round(11.46)=11
负数:Math.round(-11.46)=-11
小数点后第一位>5
正数:Math.round(11.68)=12
负数:Math.round(-11.68)=-12
小数点后第一位=5
正数:Math.round(11.5)=12
负数:Math.round(-11.5)=-11
Math.ceil(x)的使用方法:
Math.ceil(11.46)=Math.ceil(11.68)=Math.ceil(11.5)=12
Math.ceil(-11.46)=Math.ceil(-11.68)=Math.ceil(-11.5)=-11
Math.floor(x)的使用方法:
Math.floor(11.46)=Math.floor(11.68)=Math.floor(11.5)=11
Math.floor(-11.46)=Math.floor(-11.68)=Math.floor(-11.5)=-12
string不属于基本数据类型,8种基本数据类型:byte,int,short,long,float,double,char,boolean。string属于对象引用数据类型。
String,StringBuffer,StringBuilder。
String和StringBuffer、StringBuilder的区别:
String声名的是不可变的对象,每次操作都会生成新的String对象,但是StringBuffer、StringBuilder可以在原有对象基础上进行操作。因此当要操作的字符串内容需要改变时,时建议使用StringBuffer、StringBuilder。
StringBuffer和StringBuilder间的区别:
StringBuffer的线程是安全的,StringBuilder线程非安全。StringBuilder的性能高于StringBuffer。所以在单线程下建议使用StringBuilder,在多线程中建议使用StringBuffer。
功能介绍:
String:每次对String操作都会在常量池中创建新的String对象。
StringBuffer:每个StringBuffer对象都有一定的缓冲区容量,字符串大小没有超过缓冲区容量时,不会分配新的容量。但是当字符串大小超过了容量时,会自动扩容。
StringBuilder:功能与StringBuffer相同,但是少了同步锁,执行的速度加快。
StringBuffer和StringBuilder的初始容量都16。
效率:
StringBuilder>StringBuffer>String
String<(StringBuffer\StringBuilder)的原因:
string:字符串常量 String源码:public final class String{}
StringBuffer:有同步锁
StringBuilder: 没有同步锁
注意:
String str = "唐伯虎";
str = str + "点秋香";
System.out.println(str);
以上的形式并没有改变String类型变量str。因为”唐伯虎“创建了空间,”点秋香“创建了空间,”唐伯虎点秋香“又创建了新的空间。str= str + “点秋香”–>没有改变str,而是改变了常量池中常量的指向。
StringBuffer使用append在缓冲区末尾追加元素,Stringbuilder使用insert在指定点添加数据。
String str = “i”;
先检查常量池中是否已经存在,存在则引用此字符串对象,不存在则创建一个在引用此字符串对象。
String str = new String(“i”);
先检查字符串常量池中是否已经存在,如果存在,就在堆内存中创建一个新的String对象,新对象内容为”i“,但是与常量池中的是不同对象。如果不存在,则在字符串常量池中创建”i“,然后在堆内存中在创建一个新的String对象,在引用堆内存中新创建的String对象。
1.使用StringBuilder的reverse方法
public String reverse(String str){
if(str == null||str.isEmpty(){
return str;
}
StringBuilder sb = new StringBuilder(str);
sb.reverse();
return sb.toString();
}
2.使用双指针交换首尾
public String reverse(String str) {
if (str == null || str.isEmpty()) {
return str;
}
char[] charArray = str.toCharArray();
int left = 0;
int right = charArray.length - 1;
while (left < right) {
char temp = charArray[left];
charArray[left] = charArray[right];
charArray[right] = temp;
left++;
right--;
}
return new String(charArray);
}
3.使用charAt循环,放入新数组或StringBuilder中
public String reverse(String str) {
if (str == null || str.isEmpty()) {
return str;
}
char[] charArray = str.toCharArray();
char[] reverseArray = new char[charArray.length];
int right = charArray.length - 1;
int number = 0;
while (right>=0) {
reverseArray[number] = charArray[right];
number++;
right--;
}
return reverseArray;
}
4.使用递归
public String reverse(String str) {
if (str == null || str.length() <= 1) {
return str;
}
return reverseStringUsingRecursion(str.substring(1)) + str.charAt(0);
}
-
10.String类的常用方法都有哪些?
-
indexOf():返回指定字符串索引
-
charAt():返回指定索引处的字符串
-
replace():字符串替换
-
trim():去除字符串两端空白
-
split():分割字符串,返回一个分割后的字符串数组
-
getBytes():返回字符串的byte类型数组
-
length():返回字符串长度
-
toLowerCase():将字符串转换为小写
-
toUpperCase():将字符串转换为大写
-
substring():截取字符串
-
equals():字符串比较
-
-
11.抽象类必须要有抽象方法吗?
不必须
如果一个类使用了abstract关键字修饰,就是一个抽象类。
抽象类可以没有抽象方法。
一个类如果包含抽象方法,那么这个类必须是抽象类,否则编译报错。
类是抽象类,那么这个类不能被实例化。
1.普通类可以被实例化,但是抽象类不能被实例化。
2.除以上一点,抽象类他能够有构造函数,被继承时,子类一定要继承父类的一个构造方法,但是,抽象方法不能被声名为静态。
3.在抽象类中,允许普通方法有方法体,抽象方法只要声名,不需要实现。
4.有抽象方法的类,必须声名为抽象类。
5.抽象类的子类必须要实现抽象类当中的所有抽象方法,否则,这个子类也是抽象类。
6.抽象类必须abstract修饰。
7.抽象类的访问权限。抽象类可以被public、protected、默认修饰。不能被private修饰。
不能
final关键字用于修饰类、方法、变量,表示他们不可变。当一个类被声名为final时,它不能被继承,但是抽象类的主要目的就是作为其他类的基类,提供通用的接口和实现,因此抽象类必须能够被继承,由于final和抽象类的冲突,所以一个类不能同时被声名为final和abstract。
而且,抽象类中不允许有final修饰的方法。final修饰的方法不能被任何子类重写,而抽象类的方法又必须在子类中重写,两个特性也相冲突。
定义与实现
接口:
接口中只能包含抽象方法、(java8分界)默认方法、静态方法。
除了默认方法的实现,接口不能包含其他抽象方法和静态方法的实现。
所有的方法都是隐式的public和abstract。
接口可以包含常量,常量也是隐式的public static final。
抽象类:
可以包含抽象方法和具体的实现。
可以有成员变量,构造方法和初始化块。
可以有public、protected和private修饰符。
抽象方法没有方法体,但是可以有完整的实现。
继承与实现
接口:
一个类可以实现多个接口。
实现接口的类必须提供接口中的所有抽象方法的实现(除非是默认方法)。
接口之间的关系可以是继承,一个接口可以继承另一个或多个接口。
抽象类:
一个类只能继承一个抽象类(单继承)。
继承抽象类的子类必须实现抽象类中所有的未实现的抽象方法,或者子类本身也是抽象类。
抽象类可以继承另一个抽象类。
用途与设计理念
接口:
主要用于定义对象的行为或能力,而不关心这些行为的具体实现。
是一种更严格的契约,因为它不允许有具体的实现(除了默认方法)。
适用于为不相关的类提供通用功能。
抽象类:
主要用于定义一系列紧密相关的类之间的共同行为或属性。
提供了一种模板,允许子类通过继承来共享代码和实现。
适用于定义具有层次结构的类之间的关系。
实例化
接口:
接口本身不能被实例化。
实现接口的类可以被实例化。
抽象类:
抽象类本身不能被实例化。
继承抽象类的非抽象子类可以被实例化。
灵活性
接口:
由于只能包含抽象方法和常量,因此更加灵活,可以更容易地改变或扩展接口。
允许一个类实现多个接口,从而具有多重身份或能力。
抽象类:
由于可以包含具体的方法实现和成员变量,因此相对更具体,灵活性较差。
但由于可以包含构造方法和初始化块,因此可以在子类创建时进行一些初始化工作。
1、按流向分类;
输入流intput
输出流output
2、按处理数据不同分类:
字节流:二进制,可以处理一切文件,包括:纯文本,doc,音频,视频等。
字符流:文本文件,只能处理纯文本。
3、按功能不同分类:
字节流:包裹源头。按8位传输字节输入输出数据。
处理流:增强功能,提高性能。按16位输入输出数据。
BIO
是jdk1.4前的传统IO模型,是一种同步阻塞的IO,线程发起IO后,一直阻塞,知直到缓冲区数据就绪后,在进行下一步操作。
问题:无法因对高并发的场景。建立连接后,当前线程没有数据可读就会阻塞,造成资源浪费。
适用场景:客户端连接较少,并发量不高。
NIO
JDK1.4后引入,同步非阻塞的IO模型,线程发起IO后,用户线程不需要一直等待IO缓冲区,期间可以进行其他操作,只需要轮询检查IO缓冲区数据是否就绪就行。
NIO是基于Reactor反应堆模型实现的,分为三步:
-
-
注册感兴趣的事件。
-
扫描是否有感兴趣的事件发生。
-
事件发生后做出相应的处理。
-
NIO的三大组件:
-
-
Channel通道:应用程序和操作系统交互事件、传递数据的通道,channel是基于Buffer缓冲区的异步的双向的数据读写通道,即可以从Buffer中写入数据。所有被Selector注册的通道,都只能是继承了SelectableChannel类的子类,类型有:
-
FileChannel:本地文件的读取、写入、映射和操作的通道。
-
DatagramChannel:实现发送和接收UDP协议数据包的通道。
-
SocketChannel:应用服务程序的监听通道,被Channel包装的ServerSocket。
-
ServerSocketChannel:TCP Socket的监听通道,被Channel包装的Socket。
-
-
Buffer缓冲区:
主要用于和 Channel 进行交互。以写为例,应用程序先将数据写入缓冲区,再通过通道把缓冲区的数据发送出去;读也是一样,数据先从通道读到缓冲区,应用程序再读取缓冲区的数据。Buffer 缓冲区本质上是一个字节数组。
Buffer读写数据的步骤:
1、写数据到Buffer缓冲区;
2、调用flip()方法将Buffer从写模式切换到读模式;
3、从Buffer中读取数据;
4、调用clear()方法或者compact()方法清理数据,准备下一次写入。
类型有:
ByteBuffer,CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer,LongBuffer,ShortBuffer; 此外还有 MappedByteBuffer,HeapByteBuffer,DirectByteBuffer 等。
适用场景:客户端连接次数较多,连接时间较短。
AIO
JDK1.7后开始支持的异步非阻塞IO模型。线程发起IO请求后,不需要阻塞,立即返回异步IO操作,完成后会回调通知调用方。
但是,现在的IO模型使用最多的仍然是 NIO,原因很简单:Linux 系统上Java AlO 和 NIO 的底层都是基于 epoll 实现的,性能上 AIO 相比较 NIO 并没有得到很大的提升,另外,AIO 采用了异步回调、编程方式上比 NIO 要更复杂!
BIO/NIO/AIO的使用场景区别:
1、BIO适用于连接较少的场景。
2、NIO适用于连接数目多且连接时间较短的架构,比如聊天服务器。
3、AIO适用于连接次数较多且连接时间较长的架构,比如相册服务器。
BIO和NIO的区别:
BIO和NIO最大的区别并不是NIO比BIO执行速度快,而是NIO可以单个线程处理更多的连接。
java BIO面向流:
每java次从流中读取一个或多个字节,直至读取所有字节,它没有被缓存在任何地方,而且不能前后移动流中的数据。
java NIO面向缓冲区:
Java NIO 是面向缓冲区的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中0的灵活性。
阻塞I/O和非阻塞I/O
Java BIO 是基于传统的I/O模型的,当一个线程调用 read()或 write()时,该线程就会被阻塞,直到有一些数据被读取,或数据完全被写入,期间不做任何事情。而NIO,用户线程不需要一直等待IO缓冲区,期间可以进行其他操作,只需要轮询检查IO缓冲区数据是否就绪就行。
客户端连接盒服务器线程的对应关系不同
Java BIO:一个连接一个线程,客户端有连接请求时就启动一个线程进行处理。
java NIO:一个线程可以处理多个连接请求,客户端发送的请求会注册到多路复用器上,多路复用器轮询到连接有IO请求就会进行处理。
文件检查
- exists(Path path, LinkOption… options)
- 检查文件或目录是否存在。
- isDirectory(Path path, LinkOption… options)
- 检查路径是否表示一个目录。
- isRegularFile(Path path, LinkOption… options)
- 检查路径是否表示一个常规文件(非目录、非符号链接等)。
- isExecutable(Path path)
- 检查文件是否可执行。
- isReadable(Path path)
- 检查文件是否可读。
- isWritable(Path path)
- 检查文件是否可写。
文件创建和删除
- createFile(Path path, FileAttribute<?>… attrs)
- 创建一个新的空文件,如果文件已存在则抛出异常。
- createDirectory(Path dir, FileAttribute<?>… attrs)
- 创建一个新目录。
- createDirectories(Path dir, FileAttribute<?>… attrs)
- 创建所有必需的目录,但不包括已存在的目录。
- delete(Path path)
- 删除文件或目录。如果目录不为空,则抛出异常。
- deleteIfExists(Path path)
- 如果文件或目录存在,则删除它。
文件复制和移动
- copy(Path source, Path target, CopyOption… options)
- 将文件从源路径复制到目标路径。
- move(Path source, Path target, CopyOption… options)
- 将文件或目录从源路径移动到目标路径。
文件属性
- readAttributes(Path path, Class type, LinkOption… options)
- 读取文件或目录的属性。
- size(Path path)
- 获取文件的大小(以字节为单位)。
文件读取和写入
- readAllLines(Path path, Charset cs) throws IOException
- 读取文件中的所有行,并返回一个字符串列表。
- readAllBytes(Path path) throws IOException
- 读取文件的全部内容,并返回一个字节数组。
- write(Path path, byte[] bytes, OpenOption… options) throws IOException
- 将字节数组写入文件。
- write(Path path, CharSequence csq, Charset cs, OpenOption… options) throws IOException
- 将字符序列写入文件。
- newBufferedReader(Path path, Charset cs, OpenOption… options)
- 打开文件并返回一个 BufferedReader。
- newBufferedWriter(Path path, Charset cs, OpenOption… options)
- 打开文件并返回一个
BufferedWriter
。
- 打开文件并返回一个
文件锁定
- newFileLock(Path path, FileLock.Shared shared, FileChannel.OpenOption… options)
- 获取文件的文件锁。
其他
- probeContentType(Path path)
- 探测文件的MIME类型。
- walk(Path start, FileVisitOption… options)
- 遍历目录树。
- walkFileTree(Path start, FileVisitor<? super Path> visitor)
- 使用 FileVisitor 遍历文件树。
Collection接口的三个主要子类:List,Set,Queue。
List
List是一个有序Collection,可以精确掌控元素的插入顺序,并且允许重复元素的插入。
List的主要实现类:ArrayList,LinkedList,Vector等。
1、Array List :基于数组实现的动态数组,具有快速的随机访问能力,但是插入和删除操作效率较低。
2、LInked List:基于链表实现的列表,具有高效的插入和删除操作,但是随机访问效率较低。
3、Vector:与ArrayList类似,Vector的性能通常较ArrayList较差。
Set
Set是一个不含重复元素的Collection。
Set的主要实现类:HashSet,LinkedHashSet和TreeSet等。
1、HashSet:基于哈希表实现的集合,不允许重复元素,且元素是无序的。它提供了快速的查找和插入操作。
2、LinkedHashSet:具有HashSet的所有特性,同时它还维护了一个双向链表来记录元素的插入顺序。所以LinkedHashSet是有序的,迭代时会按照元素的插入顺序进行。
3、TreeSet:基于红黑树实现的集合,不允许重复元素,且元素是有序的。TreeSet会根据元素的自然顺序或指定顺序规则进行排序。
Queue
Queue是一个特殊的Collection,用于表示一个先进先出(FIFO)的数据结构。
Queue的主要实现类:PriorityQueue、ArrayQueue和LinkedList(作为Queue的实现)等。
1、PriorityQueue:优先队列,它的元素会按照优先级进行排序。优先级可以通过元素的自然顺序或提供的Comparator
来确定。
2、ArrayDeque:双端队列,支持在两端插入和删除元素。它基于数组实现,没有容量限制(在内存允许的情况下)。
3、LinkedList(作为Queue实现):也是双端队列的一种实现,支持在两端插入和删除元素。但由于它是基于链表的,所以随机访问效率较低。
Map
Map是一个接口,使用键值对存储数据。每个键都是唯一的,并且每个键都映射到一个值。
HashMap:
1、HashMap是最基础、最常用的一种Map,无序,以散列表的方式进行存储。
2、HashMap允许使用Null键和Null值。
3、HashMap线程不安全。
LinkedHashMap
1、LinkedHashMap具有HashMap的全部特性,同时它还维护了一个双向链表来记录键值对的插入顺序。
2、LInkedHashMap有序的,迭代时会按照键值对的插入顺序进行。
TreeMap
1、TreeMap基于红黑树实现的映射,不允许使用null键,但允许使用null值。
2、TreeMap会根据键的自然顺序或指定的排序规则进行排序。
WeakHashMap
1、weakHashMap的特点是,当除了自身有对键的引用外,此键没有其他引用,那么此Map
会自动丢弃此键值对。
HashTable
1、HashTable是HashMap的早期实现,继承的是陈旧的Dictionary类。
2、HashTable是线程安全的,且不允许使用null键和null值。
3、由于线程安全性的开销,HashTable的性能比HashMao差。
ConcurrentHashMap
1、ConcurrentHashMap是HashMap的线程安全版本,适用于多线程环境。
2、它提供了高效的并发访问性能,同时保证了线程安全。
Properties
1、Properties是Hashtable的子类,通常用于处理配置文件中的键值对。
2、提供了方便的方法来加载和存储属性文件。
Collection
定义:
Collection是Java集合框架中的一个根接口,表示一组对象的集合。
性质:
定义了集合的基本操作和行为,如添加、删除、遍历等。它是所有集合类的父接口,为各种具体的集合提供了最大化的统一操作方式。
功能:
主要用于存储和操作一组对象。通过Collection接口,用户可以定义各种不同的数据结构,例如List(有序集合,允许重复元素)和Set(无序集合,不允许重复元素)等。
Collections(工具类)
定义:
是Java中的一个工具类,在Java.util包中。
性质:
提供了一系列静态方法,用于对集合进行操作。这些方法通常用于对集合进行一些常见的操作,如排序列表、查找最大值、获取不可修改的集合等。
功能:
由于Collections是一个工具类,其方法都是静态的,因此可以直接通过类名调用。例如,可以使用Collections.sort()方法对列表进行排序,使用Collections.max()方法查找集合中的最大值等。
-
20.List,Set,Map之间的区别是什么?
List
有序集合,需要在集合中保持元素插入顺序时使用。元素按插入顺序存储。可重复。允许有重复元素。ArrayList、LinkedList、Vector。支持通过索引访问元素。提供ListIterator,支持双向遍历。有序(基于插入顺序)。
Set
无序且不重复。存储不重复元素式时使用,不可重复(基于hashCode和equals)。不支持通过索引访问元素。提供Iterator,单向遍历。无序(但不代表不存储顺序,而是不保证元素的存储和遍历顺序与添加顺序相同)。
Map
键值对的集合。需要通过键来快速查找时使用。存储键值对,键唯一,值可以重复。Hash Map,Tree Map(实现了SortedMap接口)。通过键值对来访问元素。提供Entry Set的迭代器,遍历键值对。TreeMap有序(基于键的自然顺序或自定义比较器),Hash Map无序。 -
21.HashMap和HashTable有什么区别?
HashMap和HashTable都是Java常见的基于哈希表实现的Map接口,都用于存储键值对映射关系。
1、数据结构
HashMap和Hashtable都是基于哈希表实现的Map接口的实现类,但是它们采用的哈希算法和数据结构有所不同。
HashMap
HashMap底层采用数组+链表/红黑树的数据结构实现,当哈希冲突时,会使用链表或则红黑树来解决冲突。HashMap中有一个负载因子(load factor)的概念,默认情况下负载因子为0.75,如果容量和负载因子的乘积大于元素个数时,就需要进行扩容操作。扩容一般是将原来的HashMap数组翻倍,再重新计算哈希码,将元素插入到新的数组中。
HaSHtable
Hashtable底层也采用数组+链表的数据结构进行实现,当哈希冲突发生时,使用链表来解决冲突。与HashMap不同的是,Hashtable在JDK 8及以前没有使用红黑树解决哈希冲突,这导致了其效率相对较低。初始容量为11,负载因子为0.75,每次扩容时容量翻倍再加1。HashTable容量可以为任意整数,最小为1。
2、线程安全
线程安全性指在多线程环境下,数据的并发访问是否会产生问题。HashMap和Hashtable在线程安全性上有所不同。
HashMap
HashMap不是线程安全的类,即多个线程同时操作HashMap可能导致出现错误的结果或者抛出ConcurrentModificationException异常。但是,可以通过Collections的synchronizedMap方法来使HashMap变成线程安全的类.Map<String, String> map = new HashMap<>(); Map<String, String> syncMap = Collections.synchronizedMap(map
**HashMap** Hashtable是线程安全的类,即多个线程同时操作Hashtable中的元素也不会产生错误的结果或者抛出ConcurrentModificationException异常。 **3、null值和null键**
null值和null键是Java中非常常见的情况,HashMap和Hashtable在处理null值和null键上也有所不同
**HashMap**
HashMap中可以存储null值和null键,但是要注意,当使用null作为键时,由于无法调用null的hashCode()方法,因此只能将其放在哈希表的第一个位置,它们是无序的。对于null值,因为可以使用null调用equals()方法,所以可以用作值。
**HashTable**
Hashtable不允许存储null值和null键,否则将会抛出NullPointerException异常。
**4、性能比较**
HashMap和Hashtable在性能上也有所不同。
HashMap
由于HashMap采用链表和红黑树的数据结构,可以更好地处理哈希冲突,因此HashMap的查找、插入和删除操作都是常数时间O(1),它的性能相对于Hashtable更高.
HashTable
Hashtable没有使用红黑树解决哈希冲突,而且所有方法都加了同步锁,相对于HashMap而言,Hashtable的效率比较低。另外,由于Hashtable不支持null键和null值,因此对其进行操作时要额外小心。Hashtable的查找、插入和删除操作平均时间复杂度为O(1),但是在极端情况下,因为哈希冲突的原因,可能会退化到O(n)。
5、应用场景
根据以上的区别和特点
HashMap
如果线程安全的Map集合,并且不需要存储null键和null值,可以使用HahTable。
如果需要高效、非线程安全的Map集合,并且需要存储null键和null值,可以使用HashMap。
如果要高效、线程安全的Map集合,可以使用ConcurrentHashMap。
**6、使用案例**
HashMap
Map<String, String> map = new HashMap<>();
map.put("apple", "red");
map.put("banana", "yellow");
map.put("orange", "orange");
String value = map.get("apple");
System.out.println(value); // 输出 red
map.remove("banana");
System.out.println(map); // 输出 {orange=orange, apple=red}
map.put(null, "nullvalue"); // 存储 null 键
System.out.println(map); // 输出 {null=nullvalue, orange=orange, apple=red}
map.put("nullkey", null); // 存储 null 值
System.out.println(map); // 输出 {nullkey=null, orange=orange, apple=red, nullkey=null}
HashTable
Hashtable<String, String> table = new Hashtable<>();
table.put("apple", "red");
table.put("banana", "yellow");
table.put("orange", "orange");
String value = table.get("apple");
System.out.println(value); // 输出 red
table.remove("banana");
System.out.println(table); // 输出 {orange=orange, apple=red}
// 存储 null 值,抛出 NullPointerException 异常
// table.put(null, "nullvalue");
// System.out.println(table);
// 存储 null 键,抛出 NullPointerException 异常
// table.put("nullkey", null);
// System.out.println(table);
1、性能需求
HashMap
插入、删除、查找的时间复杂度为O(1),这使得HashMap非常适合用于需要高效插入、删除和查找操作的场景。
哈希冲突:在最坏情况下,如果所有的键都在同一个桶中,HashMap的性能可能降为O(n),但这通常可以通过良好的哈希函数设计来避免。
TreeMap
插入、删除、查找的时间复杂度为O(log n),因为TreeMap是基于红黑树实现的。
如果操作涉及顺序访问或者需要对键进行排序,那么TreeMap会更合适.
2、顺序要求
HashMap
无序:HashMap不保证键值对的顺序,键值对的顺序可能会随时间变化。
如果顺序不重要,或者你可以接受无序存储,HashMap是更高效的选择。
TreeMap
有序:TreeMap按自然顺序(如果键实现了Comparable接口)或按提供的Comparator排序。
适用于需要按键的顺序遍历、范围查询或者对键进行排序的场景。
3、额外特性需求
HashMap
允许null键和null值:HashMap允许一个null键和多个null值。
适用于:基本的键值对存储,不需要顺序或排序的场景。
TreeMap
不允许null键:在尝试插入null键时会抛出NullPointerException。但允许null值。
适用于:需要按键排序,或需要利用SortedMap接口提供的子映射视图(如subMap、headMap、tailMap)的场景。
4、内存占用
HashMap
通常会消耗较少的内存,因为它只需要存储键值对和哈希桶,而不需要存储排序结构。
TreeMap
通常会消耗更多的内存,因为它需要维护红黑树的结构信息。
5、线程安全
HashMap
HashMap不是线程安全的。如果需要在多线程环境中使用HashMap,可以使用Collections.synchronizedMap方法或ConcurrentHashMap类。
TreeMap
TreeMap也不是线程安全的。同样,如果需要线程安全,可以使用Collections.synchronizedSortedMap方法。
-
23.HashMap的实现原理是什么?
1、数据结构
HashMap的底层数据结构是一个数组,称为“哈希桶”。每个数组元素可以是一个Entry对象(key-value对)或链表(链表保存的也是Entry对象)。在JDK 1.8及以后版本中,当链表长度超过一定阈值(默认为8)时,链表会转化为红黑树,以提高查找效率。
2、哈希函数
HashMap使用哈希函数来计算键的哈希码,这个哈希码用来决定键值对在数组中的索引位置。哈希函数的设计要尽量减少哈希冲突,即不同的键计算出相同的哈希码的情况。
3、存储过程
当向HashMap中添加一个键值对时,会首先计算键的哈希码,然后根据数组长度取模,得到在数组中的索引位置。如果该位置上没有元素,则直接将键值对存储在该位置上。如果该位置上已经存在元素,则需要进行链表或红黑树的操作:- 如果链表长度小于8,则采用链表存储,将新元素插入到链表尾部。
- 如果链表长度大于等于8,则将链表转化为红黑树进行存储。
4、哈希冲突
HashMap使用链表和红黑树来解决哈希冲突。当两个或多个键计算出相同的哈希码时,它们会被存储在同一个数组位置上,形成一个链表或红黑树。在查找时,会首先根据哈希码找到对应的数组位置,然后遍历链表或红黑树来查找目标键。
5、扩容机制
随着HashMap中元素的增多,哈希冲突会越来越频繁,影响查找效率。因此,HashMap需要进行扩容操作。扩容时,会创建一个新的数组,长度是原数组的两倍,然后将原数组中的所有元素重新计算哈希码并放入新数组中。这个操作是非常耗时的,所以如果能估算HashMap的容量,最好给它一个默认初始值,避免进行多次扩容。
当扩容前长度大于8已经转为红黑树时:
当HashMap中的数据已经转换为红黑树存储时,扩容过程会涉及红黑树的拆分和重新哈希等操作,这些操作确保了HashMap在扩容后仍然能够保持高效的性能。- 将红黑树的节点拆分成两部分,分别根据新的哈希值计算索引位置。
- 将拆分后的节点重新插入到新的数组中,可能形成新的链表或红黑树。
调整链表/红黑树:
在扩容过程中,如果某个桶下形成了链表结构,那么这个桶下的链表会根据新的索引被拆分成两个链表,分别存放在新数组的相应位置。如果链表长度超过阈值(TREEIFY_THRESHOLD,默认为8),并且数组长度大于64,则链表可能会被转换为红黑树。然而,在扩容过程中,由于数组容量已经翻倍,原有的红黑树可能会因为节点分散而不再需要,因此可能会退化为链表。
扩容完成后,HashMap会将内部引用指向新的Entry数组,旧数组将不再使用,随后垃圾回收机制会在适当的时候回收旧数组。
6、线程安全
HashMap不是线程安全的。如果在多线程环境中使用HashMap,需要额外的同步机制来确保线程安全。或者,可以使用Java提供的线程安全的Map实现,如ConcurrentHashMap。
综上所述,HashMap的实现原理基于哈希表,利用数组、链表和红黑树等数据结构来实现高效的键值对存储和查找。通过哈希函数计算键的哈希码来确定键值对在数组中的位置,并使用链表和红黑树来解决哈希冲突。同时,HashMap还提供了扩容机制来应对元素增多的情况,但需要注意线程安全性问题。 -
24.HashSet的实现原理是什么?
1、核心特点
无序:HashSet不保证存储元素的顺序,即插入的顺序和遍历时的顺序可能不一致。
唯一:HashSet保证集合中的元素没有重复,通过hashCode()和equals()方法来判断元素的唯一性。
2、底层实现
HashSet的底层实现是基于HashMap的。HashSet中的每个元素都作为HashMap的键(key)来存储,而HashMap的值(value)则是一个常量对象(通常为PRESENT)。由于HashMap的键是唯一的,这就保证了HashSet中的元素也是唯一的。
3、操作原理
(一)、添加元素
当向HashSet添加元素时,HashSet实际上是将该元素作为键存储到HashMap中,而HashMap的值则是一个固定的常量对象(PRESENT)。添加元素的过程包括计算元素的哈希值(使用hashCode()方法),找到该哈希值在哈希表中的位置(桶位置),然后调用equals()方法来检查集合中是否已经包含相同的元素。如果元素已经存在,则不会插入新的键值对,否则会在哈希表中相应位置插入新元素。
(二)、删除元素
HashSet的remove()方法用于从集合中删除指定的元素。这个方法内部调用HashMap的remove()方法,将对应的键(即元素)从HashMap中移除。
(三)、查找元素
HashSet的contains()方法用于判断集合中是否包含指定的元素。这个方法内部调用HashMap的containsKey()方法,判断HashMap中是否存在该键(即元素)。
(四)、遍历元素
HashSet的iterator()方法用于返回一个迭代器,以便遍历集合中的所有元素。这个方法内部实际上返回的是HashMap键集的迭代器。
4、哈希冲突解决
链表法:
在冲突的位置上采用链表来保存元素。当链表长度达到一定程度时(默认为8),链表会转换成红黑树。
红黑树法:
在JDK 1.8及以后版本中,当链表长度超过阈值时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉搜索树,可以确保在O(log n)时间内完成查找、插入和删除操作。
5、扩容机制
HashSet本身没有扩容机制,但是依赖底层的HashMap进行扩容。HashSet的扩容依赖于HasgMap的扩容机制。当HashMap中的元素数量超过当前容量与预设的负载因子(默认为0.75)的乘积时,HashMap会进行扩容操作,创建一个新的内部数组(bucket array),其容量通常是当前数组的两倍,并将原数组中的所有元素重新计算哈希码并放入新数组中。
6、线程安全
HashSet是非同步的,如果多个线程同时访问一个HashSet,而其中至少一个线程修改了该HashSet,那么必须保持外部同步。这通常是通过对自然封装该HashSet的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用Collections.synchronizedSet
方法来“包装”HashSet。
综上,HashSet通过基于HashMap的底层实现和哈希表的特性提供了高效的元素查找、插入和删除操作,并保证了集合中元素的唯一性。然而,它也有一些局限性和需要注意的地方,如无序性、内存开销和线程安全性等。 -
25.ArrayList和LinkedList的区别是什么?
ArrayList
-
基于数组实现。
-
数组是一块连续的存储空间,因此可以通过索引直接访问元素,查询效率较高。
LinkedList
-
基于双向链表实现。
-
链表中的元素通过节点连接在一起,每个节点包含实际存放的内容以及指向前一个和后一个节点的引用。
适用场景
ArrayList-
更适合于需要频繁进行随机访问(如通过索引获取元素)的场景。
-
当读取次数远远超过写入次数时,ArrayList是一个明智的选择。
LinkedList
-
更适合于需要频繁进行插入和删除操作的场景。
-
当添加率远高于读取率时,LinkedList更适合。
-
此外,LinkedList还可以作为队列使用,支持对集合两端的高效访问。
时间复杂度
ArrayList- 查询操作的时间复杂度为O(1),因为可以通过索引直接访问元素。
- 插入和删除操作的时间复杂度为O(n),因为在插入或删除元素时需要移动后续的元素。
LinkedList
-
查询操作的时间复杂度为O(n),因为需要从链表的头部或尾部遍历到指定的索引位置。
-
插入和删除操作的时间复杂度为O(1)(在已知索引位置的情况下),因为只需要修改元素的前后引用,而不需要移动后续元素。但需要注意,如果不知道索引位置,需要先遍历链表找到位置,此时时间复杂度为O(n)。
内存占用
ArrayList-
内存占用较少,因为数组是一块连续的存储空间,没有额外的引用开销。
-
但随着元素的增加,可能需要扩容操作,扩容机制是将原数组的内存空间长度扩容至原来的1.5倍(或其他倍数),这可能会带来一定的性能开销。
LinkedList
-
内存占用相对较多,因为每个节点都需要存储实际内容以及指向前一个和后一个节点的引用。
-
这使得LinkedList在存储相同数量元素时,通常会占用比ArrayList更多的内存空间。但是链表不需要扩容。没有因为数据增大扩容而带来的性能消耗。
线程安全
ArrayList和LinkedList都不是线程安全的。在多线程环境下,如果需要保证线程安全,可以使用Collections.synchronizedList方法将非线程安全的List转换为线程安全的List。或者,使用Java 5引入的java.util.concurrent包中的并发集合类(如CopyOnWriteArrayList)来替代。 -
-
26.如何实现数组与List之间的转换?
数组转为List
使用Arrays.asList()方法。这个方法接收一个数组为形参,并返回一个固定大小的列表(实际上是java.util.Arrays$ArrayList
,一个内部类,不是java.util.ArrayList
)。这个列表的大小是固定的,意味着你不能添加或删除元素,但你可以修改元素(如果元素是可变的)。String[] array = {"a", "b", "c"}; List<String> list = Arrays.asList(array);
当尝试添加或删除元素是,会得到UnsupportedOperationExction。
如果想要一个可以修改的List,使用new ArrayList<>(Arrays.asList(array))
来创建一个新的ArrayList
实例,并复制数组中的元素。List<String> modifiableList = new ArrayList<>(Arrays.asList(array)); modifiableList.add("d"); // 这将正常工作
List转为数组
要将List转换为数组,你可以使用List
接口的toArray()
方法。这个方法可以接受一个数组作为参数(用于指定返回数组的类型),如果不提供参数,它将返回一个Object[]
数组。List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); // 使用提供的数组类型 String[] array = list.toArray(new String[0]); // 推荐使用0长度的数组,以避免不必要的内存分配 // 或者不使用提供的数组类型(不推荐,因为返回的是Object[]) Object[] objectArray = list.toArray();
注意:
在toArray()
方法中使用一个与列表元素类型相同的空数组(例如new String[0]
)是一个常见的做法,因为这样可以避免不必要的数组创建和复制操作。Java虚拟机将能够根据需要扩展这个数组的大小以容纳列表中的所有元素。 -
27.ArrayList和Vector的区别是什么?
1、线程安全
-
Vector是线程安全的,这意味着在多线程环境中,多个线程可以同时访问并修改Vector中的数据而不会导致数据不一致。Vector通过在其方法上使用synchronized关键字来实现线程安全。
-
ArrayList则是非线程安全的,如果在多线程环境中使用ArrayList,需要手动同步对ArrayList的访问,否则可能会导致数据不一致的问题。
2、性能
-
由于Vector的每个方法都涉及同步操作,因此在单线程环境中,ArrayList的性能通常会比Vector更高,因为它避免了不必要的同步开销。
-
在多线程环境中,虽然Vector能够保证线程安全,但如果访问和操作量非常大,频繁的同步操作可能会导致性能瓶颈。
3、扩容机制、
- ArrayList和Vector都有一个初始的容量大小,当存储的元素个数超过容量时,它们都会自动扩展容量。
- ArrayList的默认初始容量为10,当容量不足时,容量会自动增加1.5倍(例如,从10增加到15)。
- Vector的默认初始容量也为10,但当容量不足时,容量会翻倍(例如,从10增加到20)。此外,Vector还允许通过构造函数指定扩展量(increment capacity),从而控制容量增长的方式。
4、遗留性
-
Vector是Java早期版本中引入的动态数组类,属于Java最早期的数据结构类之一。随着Java集合框架的发展,ArrayList逐渐成为了更常用的选择,特别是在Java 1.2引入了集合框架后。
-
因此,在现代开发中,ArrayList通常被认为是更好的选择,除非有明确的多线程需求。
5、遍历方式
-
ArrayList和Vector都支持通过Iterator和ListIterator进行遍历。
-
由于Vector是同步的,如果在遍历过程中另一个线程修改了Vector,可能会抛出ConcurrentModificationException异常。
-
对于ArrayList和Vector,建议使用for-each循环或Iterator来遍历,而不是传统的for循环,以避免在动态扩展时可能发生的错误。
ArrayList和Vector在线程安全性、性能、扩容机制、遗留性和遍历方式等方面存在显著差异。在选择使用哪个类时,应根据具体的应用场景和需求来决定。如果需要在多线程环境中使用,并且需要保证线程安全,可以选择Vector;如果在单线程环境中使用,或者不需要考虑线程安全性,并且希望获得更好的性能,可以选择ArrayList。
-