Java题集练习5(集合)
1.三种集合差别,集合类都是什么,数据结构是什么,都什么时候用
三者关系
Set集合
Set接口是Collection接口的一个子接口是无序的,set中不包含重复的元素,也就是说set中不存在两个这样的元素a1.equals(a2)结果为true。又因为Set接口提供的数据结构是数学意义上的集合概念的抽象,因此他支持对象的添加和删除。
- TreeSet:在集合中以升序对对象排序的集的实现,这意味着从一个TreeSet对象获得第一个迭代器将按照升序来提供对象,TreeSet类使用了一个TreeMap
List集合
List接口继承了Collection接口以定义一个允许重复项的有序集合。
一般有2种List,一种是基本的ArrayList,其优点在于随机访问元素,另一种是更强大的LinkedList,他并不是为了快速随机访问而设计的,而是具有一套更通用的方法
List最重要的特点就是:它保证维护元素特定的顺序,List为Collection添加了很多方法,使得能够向List中间插入和移除元素。
Map集合
Map接口不是Collection接口的继承,而是从自己的用于维护键值对关联的接口层次结构入手,按定义,该接口描述了从不重复的键到值的映射。
一般可以分为三组操作:改变,查询和提供可选视图
HashMap:
实现一个映象,允许存储空对象,而且允许键是空(由于键必须是唯一的,当然只能有一个)。
数据结构
数据结构,指计算机存储,组织数据的方式
2.Map集合:主要接口:Map 、Map.Entry 、 AbstractMap、SortedMap分别有什么用途
首先我们来看一下Map的族谱
Map.Entry 接口
Map
的entrySet()
方法返回一个实现Map.Entry
接口的对象集合。集合中每个对象都是底层Map
中一个特定的键值对。
通过这个集合迭代,您可以获得每一条目的键或值并对值进行更改。但是,如果底层Map
在Map.Entry
接口的setValue()
方法外部被修改,此条目集就会变得无效,并导致迭代器行为未定义。
AbstractMap接口
- 不可序列化:被static修饰,被transient修饰的字段
AbstractMap 是Map接口的 实现类之一,也是HashMap、TreeMap、ConcurrentHashMap 等的父类,它提供了Map 接口中方法的基本实现
抽象方法entrySet()
AbstractMap类中有一个唯一的抽象方法 entrySet() ,类中对集合视图操作的很多方法都是依赖这个抽象函数的,它返回一个保存所有 key-value 映射的Set。
当我们要实现一个不可变的 Map 时,只需要继承 AbstractMap 类并实现 entrySet() 即可。
如果想要实现一个可变的 Map ,我们还需要重写 put() 方法,因为 AbstractMap 类中默认不支持 put实现,子类必须重写该方法的实现,否则会抛出异常:
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
SortedMap
SortedMap接口:SortedMap是一个接口,继承自Map接口,它定义了对键值对按照键的自然顺序或自定义顺序进行排序的功能。SortedMap中的键值对是按照键的顺序排列的,因此可以根据键的顺序进行范围查找和遍历操作。SortedMap接口提供了一系列的导航方法和有序操作方法。
SortedMap接口的常用实现类是TreeMap,使用的是红黑树数据结构来实现有序的映射,TreeMap根据键的自然顺序或自定义比较器对键进行排序,并保持键值对的有序性
SortedMap的优缺点
优点
-
有序性
-
提供子映射:SortedMap提供了subMap方法,可以获取原有映射的子映射,方便进行范围操作
-
可自定义排序:SortedMap可以根据自然排序或自定义比较器对键进行排序,灵活性较强
缺点
-
内存占用:相比HashMap,SortedMap需要额外的内存来维护有效性,因此可能会占用较多的内存空间
-
插入和删除的性能较低:由于需要维护有效性,会略微降低性能
使用场景
-
当需要按照键的排序进行范围查找或遍历操作时
-
当需要根据键的顺序对映射进行排序时
-
当需要获取子映射进行范围操作时
3.hashset为什么要重写hashcode和equals方法?
只有两个方法都被重写才能判断两个值是否是重复的,只要有任何一个方法没有被重写,都不能判断两个值是否是重复的
4.Treeset两种实现方式,在哪用到过
自然排序(默认排序)
自然排序是TreeSet的默认排序方式,他通过在自定义的类中使用Comparable接口,重写里面的compareTo方法,就可以使TreeSet按照你所重写的方法内的规则进行排序
比较器排序
比较器排序是TreeSet的另一种排序方式,它通过TreeSet的构造方法传入一个匿名内部类的比较器给TreeSet,在内部类中定义比较排序规则给TreeSet使用
总结
1 TreeSet是通过二叉树实现的,TreeSet中的数据是自动排序的,不允许放入null值!
2 TreeSet支持两种排序方式自然排序和比较器排序,默认方式是自然排序
5.红黑树原理
红黑树的特性
(1)每个节点或者是黑色,或者是红色
(2)根节点是黑色
(3)每个叶子节点(NIL)是黑色
(4)如果一个节点是红色的,则它的子节点必须是黑色的
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点
注意
-
特性3中的叶子结点,只能是为空的结点
-
特性5 ,确保没有一条路径会比其他路径长处两倍,所以,红黑树相当于接近平衡的二叉树
红黑树的应用
红黑树的应用及其广泛,它主要用来存储有序的数据,他的时间复杂度是O(logn),效率极其的高,例如Java中的TreeSet和TreeMap
红黑树的时间复杂度定理
定理:一颗含有n个节点的红黑树高度至多为2log(n+1)
红色节点规则
不能有两个相邻的红色节点,这个规则确保了书的红色平衡,即红色节点在树中分布均匀
插入操作
当插入一个新节点时,首先按照二叉搜索树的插入规则将节点插入到合适的位置,并将节点标记为红色。然后,根据红黑树的规则进行必要的调整,以保持红黑树的性质。调整包括以下几种情况:
(1)变色:如果父节点和叔节点都是红色,将父节点和叔节点变为黑色,祖父节点变为红色。
(2)旋转:如果父节点是红色,但叔节点是黑色或缺失(NIL节点),且新节点是父节点的右子节点,或者父节点是祖父节点的左子节点且新节点是父节点的左子节点,进行相应的旋转操作来调整节点位置。
删除操作
当删除一个节点时,首先按照二叉搜索树的删除规则删除节点,并用其后继节点或前驱节点来替代被删除的节点。然后,根据红黑树的规则进行必要的调整,以保持红黑树的性质。调整包括以下几种情况:
(1)兄弟节点为红色:通过旋转操作将兄弟节点变为黑色,然后重新调整。
(2)兄弟节点为黑色且其子节点都为黑色(包括NIL节点):将兄弟节点变为红色,然后将当前节点上移作为新的当前节点。
(3)兄弟节点为黑色且至少有一个子节点为红色:通过旋转操作调整节点位置。
通过这些规则和调整操作,红黑树保持了平衡性,确保了树的高度始终保持在较小的范围内,使得插入、删除和查找操作的时间复杂度保持在O(log n)级别。这使得红黑树在许多应用中成为一种高效的数据结构选择,如C++ STL中的map和set,Linux系统的虚拟内存的管理就是基于红黑树实现的。
6.ArrayList和LinkedList各自什么时候用
-
ArrayList:由数组实现的List,允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。
-
LinkedList:对顺序访问进行了优化,向List中间插入与删除的开销并不大,随机访问则相对较慢,(使用ArrayList代替)还有下列方法:addFirst(),addLast(),getFirst(),getLast(),removeFirst(),romoveLast().这些方法使得LinkedList可以当作堆栈,队列和双向队列使用。
7.Map两种解析方式
Map中键值对的两种取出方式
- Set keySet
将map中所有的键存入到set集合,因为set内部具有迭代器,可以根据迭代器取出所有的键,在调用get方法获取每个键对应的值
- Set<>Map.entry<K,V>> entrySet
将map集合中的映射关系存入到了set集合中,而这个关系的数据类型就是Map.entry,Entry其实就是Map中的一个static内部接口,为什么要定义在内部呢?因为只有有了Map集合,有了键值对,才会有键值的映射关系;关系属于Map集合中的一个内部事物,而且该事物可以直接访问Map集合中的元素
8.迭代器模式
定义
迭代器模式为遍历不同的容器结构提供统一的接口,提供一种方法顺序访问聚合对象中的各个元素,而又不暴露聚合对象内部的表示,迭代器模式的核心思想是数据的存储和遍历分离开来,同其他设计模式类似,迭代器的分离操作的优缺点共存
- 优点
聚合对象只负责存储,遍历由迭代器完成,可以将聚合对象保护起来,外部用户使用只需要调用迭代器就可以
- 缺点
遍历操作从聚合对象中剥离出去,由单独的迭代器类完成,这会增加类的数量,进而增加系统的复杂性
模式结构
迭代器模式主要包含以下角色:
1 抽象迭代器:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
2 具体迭代器:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
3 抽象聚合:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
4 具体聚合:实现抽象聚合类,返回一个具体迭代器的实例。
这四个结构的关系如下图所示
适用场景
顾名思义,迭代器用于迭代聚合数据类型,java中的聚合数据类型实际是指集合框架。集合框架中广泛使用迭代器模式。
由于java的集合框架提供的功能基本满足日常开发需要,一般不会在设计新的容器类,也就不会实现对应的迭代器。若需要自定义实现聚合类型,则同样需要提供该集合的迭代器。
代码实践
迭代器模式的结构比较简单,根据模式结构创建对应的类即可,为突出重点,不适用泛型,直接假设聚合对象中存储的是Student类,因此只需要创建五个类即可
抽象迭代器接口
/**
集合迭代器接口
该类定义了两个实现迭代操作的两个关键方法
*/
public interface AggregateIterator {
Student nextStudent();
boolean isLastStudent();
}
具体迭代器实现类
/**
学生集合类的迭代器的实现接口
*/
public class StudentAggregateIteratorImpl implements AggregateIterator {
private List<Student> studentList;
int position;
Student student;
public StudentAggregateIteratorImpl(List<Student> studentList){
this.studentList = studentList;
}
@Override
public Student nextStudent() {
student = studentList.get(position);
position++;
return student;
}
@Override
public boolean isLastStudent() {
return position < studentList.size();
}
}
抽象聚合接口
/**
学生集合接口,定义存储Student类的接口
*/
public interface StudentAggregate {
//添加学生方法
void addStudent(Student student);
//删除学生方法
void removeStudent(Student student);
//获取迭代器方法
AggregateIterator getStudentIterator();
}
具体的聚合实现类
/**
学生集合接口实现类
*/
public class StudentAggregateImpl implements StudentAggregate{
private List<Student> studentList;
public StudentAggregateImpl(){
this.studentList = new ArrayList<Student>();
}
//实现添加学生方法
@Override
public void addStudent(Student student) {
studentList.add(student);
}
//实现删除学生方法
@Override
public void removeStudent(Student student) {
studentList.remove(student);
}
//返回一个迭代器实例
@Override
public AggregateIterator getStudentIterator() {
return new StudentAggregateIteratorImpl(studentList);
}
}
存储元素Student类
/**
学生类
*/
public class Student {
private String name;
public Student(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
9.Collections和Collection的差别
Collection
Collection是集合类的上层接口,本身是一个Interface,里面包含了一些集合的基本操作
Collection接口是Set接口和List接口的父接口
Collections
Collections是一个集合框架的帮助类,里面包含一些对集合的排序,搜索以及序列化的方法
最根本的是Collections是一个类,而Collection是一个接口
10.Collections类常用的方法及demo
Collections常用的静态方法
-
sort()排序方法
-
binarySearch()二分查找方法
-
max()/ min()查找最大值或最小值方法
-
reverse()反转元素顺序方法
代码演示
public class CollectionsUseDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("ant");
list.add("bear");
list.add("pen");
list.add("zoo");
list.add("apple");
list.add("candy");
list.add("zookeeper");
System.out.println("*************");
for (String s : list) {
System.out.println(s);
}
System.out.println("**************");
Collections.sort(list);
for (String s : list) {
System.out.println(s);
}
//查找集合元素的最大、最小值
System.out.println("集合中的最大元素:" + Collections.max(list));
System.out.println("集合中的最小元素:" + Collections.min(list));
System.out.println("***************");
//查找集合中的特定元素
System.out.println(Collections.binarySearch(list, "candy"));
//反转集合元素的顺序
Collections.reverse(list);
for (String s : list) {
System.out.println(s);
}
}
}
Collections实现Comparable接口自定义比较规则
Collections可以实现Comparable接口并重写接口中的compareTo()方法来自定义比较规则比较元素的大小
* @Description//学生类型:学号、姓名和性别
*/
public class Student implements Comparable {
private int no;
private String sex;
private String name;
public Student() {
}
public Student(int no, String name, String sex) {
this.no = no;
this.sex = sex;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//重写方法:定义学生对象的比较规则
//比较规则:按学号比,学号大的同学往后排,学号小的同学往前排
//比较对象:当前学生对象(this)和Object o
@Override
public int compareTo(Object o) {
Student student = (Student) o;
if (this.no == student.no) {
return 0;//学号相同,两个学生对象一般大
} else if (this.no > student.no) {
return 1;//当前学生对象学号大于比较的学生对象学号
} else {
return -1;//当前学生对象学号小于比较的学生对象学号
}
}
}
11.数组和List相互转换的方法及demo
数组转List
我们可以使用Arrays类中的asList()方法来将数组转换为集合。注意:基本类型数组转换为List集合需要使用包装类
List转数组
使用toArray()方法可以使集合转换为Object类型的数组
代码演示
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TransForm {
public static void main(String[] args) {
//集合转化为数组
List list = new ArrayList();
list.add("123");
System.out.println(list);
Object[] arr = list.toArray();
for (Object i:arr
) {
System.out.println(i);
}
//数组转化为集合,基本类型数组转换为集合时需要使用包装类
Integer[] integers = {1,2,3,4,5};
List intList = Arrays.asList(integers);
System.out.println(intList);
}
}
12.遍历数据库结果集时会遇到的问题
我们在遍历数据库的结果集合时,可能会有如下代码书写形式:
ArrayList<user> al = new ArrayList<user>();
User user =new User();
for()
{
user.setName();
.....
...... al.add(user);
}
这种写法将会导致所有用户对象共享同一个 User
实例。每次循环时,都会修改同一个对象的属性,然后将其添加到列表中。最终的结果将是列表中只有一个对象的不同版本。
问题分析
每次循环时,user
对象的引用始终指向的是同一个对象实例。因此,每次调用 al.add(user);
都是在向列表中添加同一个对象。这将导致列表中所有的元素都是最后一个用户的实例
内存图示例
1 初始化阶段
+-------------------+ +---------------------+
| ArrayList<User> al| --> | null |
| | +---------------------+
+-------------------+
+---------------------+
| User user | --> | null |
+---------------------+ +---------------------+
2 第一次循环
+-------------------+ +---------------------+
| ArrayList<User> al| --> | [User@Address] |
| | +---------------------+
+-------------------+
+---------------------+ +--------------+
| User user | --> [User@Address] | name="Alice"
+---------------------+ +--------------+
3 第二次循环
+-------------------+ +---------------------+
| ArrayList<User> al| --> | [User@Address] |
| | +---------------------+
+-------------------+
+---------------------+ +--------------+
| User user | --> [User@Address] | name="Bob"
+---------------------+ +--------------+
4 第三次循环
+-------------------+ +---------------------+
| ArrayList<User> al| --> | [User@Address] |
| | +---------------------+
+-------------------+
+---------------------+ +--------------+
| User user | --> [User@Address] | name="Charlie"
+---------------------+ +--------------+
最终,al
中的所有元素都将是指向同一个 User
对象的引用,而该对象的属性最后一次被修改为 name="Charlie"
正确做法
为了避免这个问题,应该在每次循环时都创建一个新的User实例
ArrayList<User> al = new ArrayList<>();
for (...) {
User user = new User();
user.setName(...);
...
al.add(user);
}
这样每次循环时都会创建一个新的 User
实例,从而保证每个用户对象都是独立的。
13.线程安全的集合有哪些,线程安全的list的特点
在Java中,线程安全的集合是指在多线程环境下可以直接使用而不需要额外的同步机制(如synchronized
关键字或ReentrantLock
等)就能保证数据一致性、完整性的集合类。Java提供了多种线程安全的集合实现,适用于不同的场景。
线程安全的集合
-
Vector
:这是Java中最古老的线程安全的集合之一,它的方法都是同步的。但是由于Vector
在JDK 1.2之后已经不再推荐使用,因为它提供的同步机制不够灵活,效率较低。 -
LinkedList
的线程安全版本:可以通过Collections.synchronizedList(new LinkedList<T>())
创建一个线程安全的LinkedList
。 -
ArrayList
的线程安全版本:同样,可以使用Collections.synchronizedList(new ArrayList<T>())
创建一个线程安全的ArrayList
。 -
ConcurrentHashMap
:虽然这是一个映射(Map
),但它是线程安全的,并且提供了高性能的并发访问。 -
CopyOnWriteArrayList
:这是一个特殊的线程安全的List
实现,它通过“写入时复制”的策略来实现线程安全。这种方法适用于读多写少的场景,因为在写操作时需要创建新的数组副本,因此写操作的性能较差。
线程安全的List
特点
CopyOnWriteArrayList 特点
- 读写分离:读操作不会阻塞写操作,反之亦然。读操作总是从旧数组中读取数据,而写操作则创建新数组并在新数组中进行写入。
- 适合读多写少场景:因为写操作涉及到创建新数组,所以如果写操作频繁,那么性能会受到影响。
- 不可迭代修改:一旦创建了
Iterator
,就不能再修改CopyOnWriteArrayList
,否则会抛出ConcurrentModificationException
。 - 高延迟:由于写操作涉及到数组的复制,所以写操作可能会有较高的延迟。
同步包装的ArrayList
或LinkedList
特点
- 方法同步:所有的方法都被同步,这意味着每次调用方法都需要获得锁。
- 全局同步:由于所有的操作都是在一个锁上进行同步的,所以即使是并发读取也会受到限制。
- 性能瓶颈:由于每次访问都需要锁定整个集合,所以在高并发环境下性能较差。
选择合适的线程安全集合
选择合适的线程安全集合取决于具体的应用场景:
- 如果写操作较少,读操作较多,可以选择
CopyOnWriteArrayList
。 - 如果需要在写操作频繁的情况下保证线程安全,可以考虑使用
Collections.synchronizedList
来包装ArrayList
或LinkedList
。 - 如果需要更加灵活的线程安全解决方案,可以使用自定义的同步策略,如使用
ReentrantLock
等。
14.泛型如何提供安全监测机制,什么是类型擦除(删除)
泛型如何提供安全监测机制
在Java中,泛型是用来创建参数化的类型的机制,它允许在编译时进行类型检查,从而提供类型安全性。以下是泛型如何提供安全监测机制的一些要点:
-
类型参数:使用泛型时,可以在类或接口声明时定义类型参数,例如
List<T>
中的T
。 -
类型参数的实例化:在创建泛型类型的实例时,需要指定具体的类型参数,例如
List<Integer>
。 -
编译时类型检查:编译器会在编译时检查类型参数是否正确使用,如果尝试向
List<Integer>
中添加非Integer
类型的对象,编译器会报错。 -
类型安全的强制转换:编译器会自动插入必要的强制转换,使得泛型代码在运行时能够正确工作。
-
通配符:使用通配符(如
?
)可以增加代码的灵活性,同时保持类型安全性。例如,List<?>
表示可以接受任何类型的List
,但不允许添加元素。 -
限定通配符:可以使用带有上限或下限的通配符(如
List<? extends Number>
或List<? super Integer>
)来进一步限制类型。 -
类型擦除:尽管泛型在编译时提供了类型安全,但在运行时,所有的类型参数都会被擦除为其对应的原始类型(如
List
)。这意味着在运行时,你无法直接访问泛型类型的特定信息,但编译时的类型安全仍然得到了保障。
什么是类型擦除(删除)
类型擦除是指在Java运行时环境中,所有的泛型信息都会被“擦除”,即在编译后的字节码中,泛型类型信息被替换成了它们对应的原始类型(非泛型类型)。这意味着在运行时,你无法知道一个泛型类型的特定信息,因为所有的类型参数都会被替换为它们的原始类型。
类型擦除的影响
-
运行时类型信息丢失:在运行时,无法直接获取泛型参数的类型信息。例如,
List<Integer>
和List<String>
在运行时都被视为List
。 -
强制类型转换:编译器会在适当的地方插入强制类型转换,以确保类型安全。
-
数组和泛型的限制:由于类型擦除的存在,无法创建一个泛型数组(如
T[]
),因为这会导致类型擦除后变成Object[]
,从而失去类型安全性。
示例
import java.util.List;
public class GenericExample {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<Integer>();
intList.add(1); // 编译器自动插入强制转换
// 以下代码在编译时不通过
// List<String> stringList = intList; // 编译错误
// 以下代码在编译时通过,但在运行时会抛出ClassCastException
// List<String> stringList = (List<String>) intList;
// 使用通配符
List<?> wildcardList = intList; // 编译通过,运行时被视为List
}
}
总结来说,泛型通过编译时的类型检查来提供类型安全,而在运行时,所有的泛型信息都被擦除,只保留原始类型。这种机制使得泛型在提供类型安全性的同时,又不会增加运行时的复杂性。