自我介绍
Java集合,常见的实现类
ArrayList: 动态数组,可以根据需要动态扩容,底层是数组,查询快,增删慢。
LinkedList: 双向链表,增删快,查询慢。
LinkedHashMap: 基于HashMap和双向链表实现,可以保证插入顺序和访问顺序一致。
priorityQueue: 优先队列,底层是堆,可以用来实现堆排序。
ConcurrentHashMap: 基于散列表和链表实现,线程安全,支持高并发。
HashMap:基于哈希表的键值对集合,可以通过键快速查找值
TreeMap:基于红黑树的键值对集合,可以对键进行排序
HashSet:基于哈希表的集合,不保证元素的顺序
TreeSet:基于红黑树的集合,可以对元素进行排序
Set的底层实现
Set的底层实现通常使用哈希表(Hash Table)来实现。
哈希表是一种基于键值对(Key-Value Pair)的数据结构,它通过将键映射到桶(Bucket)中,将值存储在桶中。在Set中,键就是元素,值就是布尔值True或False,表示该元素是否存在于集合中。
Set的底层实现包括以下步骤:
初始化一个空的哈希表。
将元素添加到哈希表中。对于每个元素,计算其哈希值,并将该元素作为键,将布尔值True作为值,存储在哈希表中对应的桶中。
判断元素是否存在于集合中的时候,也通过计算元素的哈希值,并查找该哈希值对应的桶来实现。如果该桶中存储的值是True,则表示该元素存在于集合中,否则表示该元素不存在于集合中。
需要注意的是,由于哈希表可能会出现哈希冲突(Hash Collision),即不同的键计算出相同的哈希值,因此Set的底层实现需要处理哈希冲突的情况。常见的处理哈希冲突的方法有开放寻址法(Open Addressing)和链地址法(Chaining)等。
总之,Set的底层实现主要依赖于哈希表来实现,通过将元素存储在哈希表中,实现了快速查找和判断元素是否存在的操作。
HashSet如何处理重复数据
HashSet是一种基于哈希表的数据结构,它不允许存储重复元素。因此,当尝试将重复元素添加到HashSet时,它会忽略该元素,并不会报错。在内部实现中,HashSet通过计算元素的哈希值来存储元素,并将元素存储在对应的桶中。如果两个不同的元素具有相同的哈希值,HashSet会使用链地址法或开放寻址法等技术来处理哈希冲突。但是,即使在这种情况下,HashSet仍然不允许存储重复元素,因此重复的元素会被忽略。因此,当向HashSet添加元素时,不需要担心重复元素的处理方式,因为HashSet会自动忽略重复元素,并保持集合中元素的唯一性。
多线程操作Set,线程安全如何实现?
- 使用线程安全的Set实现类:Java提供了一些线程安全的Set实现类,如ConcurrentHashMap、CopyOnWriteArraySet等,可以直接使用这些类来保证线程安全。
- 使用synchronized关键字:在多线程操作Set时,可以将添加、删除和查询等操作方法使用synchronized关键字进行同步,保证在同一时间只有一个线程能够访问Set。
Set的遍历JAVA
- for-each循环:使用for-each循环遍历Set,需要先将Set转换为数组,然后再使用for-each循环遍历数组。
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("C");
String[] array = set.toArray(new String[0]);
for (String s : array) {
System.out.println(s);
}
- 迭代器:使用迭代器遍历Set,需要先通过Set的iterator()方法获取迭代器,然后使用while循环和next()方法遍历Set。
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("C");
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
}
如果需要按一定顺序遍历set,可以将其先转化为List,然后再遍历List。
自定义排序如何实现
- 实现Comparable接口
public class Student implements Comparable<Student> {
private String name;
private int age;
private int score;
public Student(String name, int age, int score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public int compareTo(Student o) {
return Integer.compare(this.score, o.score);
}
}
- 实现Comparator接口
public class StudentComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return Integer.compare(o1.getScore(), o2.getScore());
}
}
set和list的区别,使用场景
- Set是一种集合,它不允许存储重复元素,通常用于存储不重复的对象。
- list是一种序列,它允许存储重复元素,通常用于存储有序的对象。
arraylist动态扩容实现
ArrayList的底层实现是数组,当数组容量不足时,需要对数组进行扩容。ArrayList的扩容依赖于ensureCapacityInternal()方法和grow()方法。
过程如下:
- 首先判断当前存储元素容量是否到了最大大小,也就是Integer.MAX_VALUE - 8,如果到了最大大小,则不再进行扩容。否则当前存储元素的数组容量已经满了时就会开启扩容。
- ensureCapacityInternal()方法会先计算出新的数组容量,新的数组容量为当前数组容量的1.5倍,然后再判断新的数组容量是否大于需要的最小容量,如果大于最小容量,则直接使用新的数组容量,否则使用最小容量。
- grow方法会创建一个新的存储元素的数组,然后将原数组中的元素复制到新数组中,最后将新数组赋值给ArrayList的elementData属性。
arraylist的addAll方法,如果容量为1,addAll一个容量为100000的数组,怎么扩容?(addAll底层)
- 计算新的容量大小,新容量大小为原容量的1.5倍加上要添加的元素个数。
- 创建一个新的数组,将原数组中的元素复制到新数组中。
- 将要添加的元素从原数组中复制到新数组中。
- 将新数组设置为ArrayList的底层数组。
- 更新ArrayList的容量和元素个数。
在这个例子中,如果ArrayList的容量为1,而要添加的数组的容量为100000,那么ArrayList会进行扩容。新容量大小为1.5 * 1 + 100000 = 100001。然后会创建一个新的容量为100001的数组,并将原数组中的元素复制到新数组中。接着将要添加的数组中的元素也复制到新数组中。最后将新数组设置为ArrayList的底层数组,并更新ArrayList的容量和元素个数。
hash因子和哈希冲突
- hash因子:hash因子是一个常数,它用于计算哈希值。在计算哈希值时,会将元素的哈希码与hash因子进行异或运算,然后再计算出元素的哈希值。hash因子的值可以任意指定,但是为了减少哈希冲突,通常会选择一个质数作为hash因子。
- 哈希冲突:哈希冲突是指不同的元素具有相同的哈希值。在哈希表中,哈希冲突是不可避免的,因为哈希表的容量是有限的,而元素的个数是无限的。因此,当元素的个数超过哈希表的容量时,就会出现哈希冲突。哈希冲突会影响哈希表的性能,因为哈希冲突会导致元素存储在哈希表的同一个桶中,从而导致哈希表的查询性能下降。因此,减少哈希冲突是哈希表设计的一个重要目标。
hash冲突发生的原因
哈希表的容量是有限的,而元素的个数是无限的。因此,当元素的个数超过哈希表的容量时,就会出现哈希冲突。解决hash冲突的4种方式:
开放寻址法。
链地址法。
哈希表扩容。
二次哈希。
常见的hash函数
- MD5:MD5是一种哈希算法,它可以将任意长度的数据转换为长度固定的数据,通常为128位。
- SHA:SHA是一种哈希算法,它可以将任意长度的数据转换为长度固定的数据,通常为160位。有很多变种,如SHA-1、SHA-256、SHA-512等。
- ELF Hash:ELF Hash是一种哈希算法,它可以将任意长度的数据转换为长度固定的数据,通常为32位。它是Linux系统中使用的一种哈希算法。将符号映射为索引
- jenkins Hash:jenkins Hash是一种哈希算法,它可以将任意长度的数据转换为长度固定的数据,通常为32位。它是jenkins CI系统中使用的一种哈希算法。用于构建Jenkins的ID和项目版本号
- MurmurHash:MurmurHash是一种哈希算法,它可以将任意长度的数据转换为长度固定的数据,通常为32位或64位。它是一种高性能的哈希算法,适用于一般的哈希检索操作。用于分布式哈希表
- FNV Hash:用于快速计算哈希值。
- 基数为质数而非2的哈希函数,例如基数为33、65、127、2127的哈希函数等。
hashmap扩容机制
HashMap在Java中是一个用于存储键值对的数据结构。其内部实现采用了数组+链表(Java 8 之后为数组+链表+红黑树)的结构。
在插入元素的时候,根据元素的key进行hash,然后根据得到的hash值确定元素在数组中的位置。如果元素数量过多,原有的数组无法容纳,就会进行扩容。
扩容过程是HashMap容量变为原来的两倍加1,原有的元素会被重新计算hash值并复制到新的数组中,同时新开辟的数组位置为null的元素数量也为原来的两倍加1。这个过程的时间复杂度为O(n)。
在Java 8及以后的版本,当链表长度超过一定阈值(8或者8/0.75(装载因子))时,会将链表转换为红黑树,这样可以提高查找的效率。当红黑树的节点数小于6时,会退化为链表,这样可以保证链表的插入和删除操作不会因为节点的移动而变得复杂。
hashmap为何使用红黑树
HashMap使用红黑树的主要原因是为了解决哈希冲突问题。当HashMap中的元素数量较少时,使用链表来解决冲突是比较合适的,因为链表的插入和删除操作效率较高。但是当元素数量增多时,链表的效率会变得很低,因为查找一个元素需要遍历整个链表。
为了解决这个问题,当链表中的元素数量达到一定阈值时,HashMap会将链表转换为红黑树。红黑树是一种自平衡的二叉查找树,它的插入、删除和查找操作的平均时间复杂度都是O(log n),比链表的效率高很多。
使用红黑树可以提高HashMap的性能,特别是在处理大量数据的情况下。但是红黑树的插入、删除和查找操作的常数时间比链表大,所以只有在元素数量较大时才使用红黑树,以平衡时间和空间的消耗。
红黑树的调整,相比平衡二叉树为什么实际应用优秀
红黑树相比平衡二叉树在实际应用中更优秀,原因在于红黑树的调整复杂度为O(log n),而平衡二叉树的调整复杂度为O(n)。这意味着在插入或删除节点时,平衡二叉树需要调整的次数可能比红黑树多很多,尤其是当树的高度较大时。而红黑树可以在常数时间内完成调整,使得其在实践中的性能更优秀。
此外,红黑树还有一些性质,如五个约束条件,使得红黑树在插入和删除操作时能够保持相对平衡的状态,而且不会像AVL树那样频繁地进行旋转操作。因此,红黑树在实际应用中具有更好的性能和更广泛的应用。
Java的特性
- 跨平台/可移植性:这是Java的核心优势。Java在设计时就很注重移植和跨平台性。
- 安全性:Java适合于网络/分布式环境,为了达到这个目标,在安全性方面投入了很大的精力,使Java可以很容易构建防病毒,防篡改的系统。
- 面向对象:面向对象是一种程序设计技术,非常适合大型软件的设计和开发。
- 简单性:Java就是C++语法的简化版,我们也可以将Java称之为“C++-”。
- 高性能:Java最初发展阶段,总是被人诟病“性能低”;客观上,高级语言运行效率总是低于低级语言的,这个无法避免。
- 分布式:Java是为Internet的分布式环境设计的,因为它能够处理TCP/IP协议。
- 健壮性:Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。
- 解释执行:Java程序在运行时会解释字节码,并将它映射到特定机器的机器码上执行。
Default修饰符和Protected修饰符的区别
访问权限 。Default修饰的属性和方法只能在同一包中进行访问,Protected修饰的属性和方法可以被类本身的方法及子类访问。
是否允许跨包访问 。Default修饰的属性和方法不允许跨包访问,Protected修饰的属性和方法允许跨包访问。
Java中修饰符的权限从大到小依次为:public、protected、default、private。
抽象类和接口的区别,使用场景
相同点:
- 都不能被实例化
- 都可以定义抽象方法,子类/实现类必须实现抽象方法
不同点 - 抽象类可以有构造方法,接口不能有构造方法
- 抽象类可以包含非抽象方法,接口不能包含非抽象方法(Java8之后可以)
- 抽象类只能单继承,接口可以多实现
- 抽象类可以定义成员变量,接口不能定义成员变量,只能是public static final修饰的静态常量
使用场景: - 抽象类:向约束子类必须实现某些方法,又不想让外部创建抽象类的实例,可以使用抽象类。
- 接口:想要实现多继承,可以使用接口。
// 抽象类
public abstract class Animal {
public abstract void eat();
public abstract void sleep();
}
// 接口
public interface Animal {
void eat();
void sleep();
}
// 分别使用抽象类和接口
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eat");
}
@Override
public void sleep() {
System.out.println("Dog sleep");
}
}
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog eat");
}
@Override
public void sleep() {
System.out.println("Dog sleep");
}
}
抽象类和抽象方法抽象字段之间的因果关系
抽象类和抽象方法、抽象字段之间的因果关系如下:
抽象方法所在类一定是抽象类。
抽象类里面可以没有抽象方法,可以全是普通方法。
抽象类不能创建对象,也就是说不能对象实例化。
当子类继承抽象父类时,如果没有完全实现父类中的抽象方法,则该子类必为抽象类。
抽象父引用指向子类,抽象父类不能调用子类所特有的属性和方法,只能调用抽象父类声明的属性和方法。如果调用父类的抽象方法,实际上是调用子类的方法。如果调用的不是抽象方法,子类没重写调用父类,子类重写调用子类。
abstract关键字可以修饰什么
可以修饰类,方法。
当修饰类时,该类为抽象类,不能被实例化,只能被继承。
当修饰方法时,该方法为抽象方法,只有方法的声明,没有方法的实现,子类必须实现该方法。
枚举类,可以new出来吗?
不可以,枚举类是用于定义有限个特定的对象,而不是由用户创建多个实例。枚举类中的每个对象都是预先定义好的,并且都是静态的,要使用枚举类中的对象,可以直接通过类名引用。不能在枚举类中定义非静态方法,但是可以定义构造函数和其他实例变量和方法,但是这些方法通常用于初始化对象的状态。
反射,反射的应用,反射存在的问题
反射是指运行时动态获取类信息,接口信息,字段和方法信息,并能够动态调用对象方法的技术。
应用:
- 框架设计的灵魂,例如spring,mybatis等框架都是基于反射实现的。
- 动态代理设计模式
- 反射机制可以实现动态加载类和方法,动态调用方法。
存在的问题:
- 性能问题:反射调用方法的性能比直接调用方法的性能要低很多。
- 安全问题:反射可以调用任意方法,包括私有方法,这样就会破坏类的封装性,导致安全问题。
- 内部暴露问题:反射可以调用任意方法,包括私有方法,这样就会暴露类的内部实现,导致内部暴露问题。
为什么要用ES和Redis
ES(Elasticsearch)和Redis是两种不同的开源软件,用于不同的用途。
Elasticsearch(ES)是一个分布式搜索和分析引擎,它可以快速地存储、搜索和分析大量的数据。它被广泛用于构建实时搜索、日志分析、数据分析和监控等应用。ES具有以下特点:
- 高性能:ES使用倒排索引和分布式架构,可以快速地搜索和分析大量的数据。
- 可扩展性:ES可以通过添加更多的节点来扩展存储和处理能力。
- 多种查询方式:ES支持全文搜索、过滤、聚合等多种查询方式,可以满足不同的搜索和分析需求。
- 实时性:ES可以在数据写入后几乎立即可用,适用于实时搜索和分析场景。
Redis是一个内存数据结构存储系统,它支持多种数据结构(如字符串、哈希表、列表、集合、有序集合等),并提供了丰富的操作命令。Redis具有以下特点:
- 高性能:Redis将数据存储在内存中,可以快速地读取和写入数据。
- 数据持久化:Redis支持将数据持久化到磁盘,以防止数据丢失。
- 缓存:Redis常用于作为缓存系统,可以提高应用程序的性能。
- 发布/订阅:Redis支持发布/订阅模式,可以用于构建实时消息系统。
- 分布式锁:Redis提供了分布式锁的实现,可以用于解决并发访问的问题。
为什么要同时使用ES和Redis取决于具体的应用场景和需求。通常情况下,ES用于存储和搜索大量的结构化和非结构化数据,而Redis用于缓存、计数、排行榜等需要快速读写的场景。同时使用ES和Redis可以充分发挥它们各自的优势,提高系统的性能和可扩展性。
es索引和b+树索引的区别
ES(Elasticsearch)索引和B+树索引是两种不同的索引结构,它们在实现方式和适用场景上有所不同。
B+树索引是一种常见的数据库索引结构,它是一种平衡树结构,可以用于快速查找和排序。B+树索引将数据按照一定的规则分成多个节点,每个节点包含多个关键字和指向下一级节点的指针。B+树索引的查找和插入操作的时间复杂度为O(log n),适用于静态数据和范围查询。
ES索引是一种基于倒排索引的数据结构,它将文档中的每个词都映射到包含该词的文档列表中。ES索引的查找操作是通过查询词在倒排索引中的位置来实现的,因此查找速度非常快。ES索引适用于动态数据和全文检索。
下面是ES索引和B+树索引的一些区别:
数据结构不同:ES索引是基于倒排索引的数据结构,而B+树索引是一种平衡树结构。
适用场景不同:ES索引适用于动态数据和全文检索,而B+树索引适用于静态数据和范围查询。
查询方式不同:ES索引的查询方式是通过查询词在倒排索引中的位置来实现的,而B+树索引的查询方式是通过比较关键字的大小来实现的。
存储方式不同:ES索引将文档中的每个词都映射到包含该词的文档列表中,而B+树索引将数据按照一定的规则分成多个节点,每个节点包含多个关键字和指向下一级节点的指针。
需要注意的是,ES索引和B+树索引都是常见的索引结构,它们各有优缺点,应根据具体的应用场景选择合适的索引结构