Object 下有哪几个方法?
wait , notify ,notifyAll , equals, hascode , toString , getClass , finalize , clone()
equals 方法, hascode 方法的区别,为什么重写equals 方法就要重写hascode方法?
- equals 和 hascode 方法都是object 下的方法, 很多类对象都会重写这两个方法,因为是为了维护一个约定:
- 1、如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;
-
2、如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)
遵从这个方法,在比较两个对象就可以先比较hashcode是否相同,相同在比较equals,这样可以大大提高效率。
在集合中, 放入一个新的元素,为了提高性能效率,会先比较对象的hashcode 是否有,没有则加入,有则再比较equals方法,
这样可以大大提高效率, 如果不重写hashcode方法,可能会导致使用集合出现问题
java 中 == 和 equals 的区别?
- 如果是基础类型,== 比较的是两个数字的值, equals 是基础类型的包装类,也是比较的值
- 如果两个是String 类型 , String 重写了equals 方法,会将字符串转换成char[] 进行比较,比较的也是值
- == 比较必须是两个相同类型对象比较 , equals 可以两个不同类型对象比较
- 如果比较的是两个引用对象 ,==比较的是引用的内存地址的值,equals比较的是对象的值
String aa = new String("a");
String bb = new String("a");
System.out.println(aa.equals(bb)); true
System.out.println(aa==bb); false
Blog blog=new Blog();
blog.setId(1L);
Blog blog1=new Blog();
blog1.setId(1L);
System.out.println(blog.equals(blog1)); -- true
System.out.println(blog==blog1); -- false
String,StringBuffer,StringBuilder有什么区别?
-
String是Java中基础且重要的类,被声明为final class,两个特点:1、该类不可被继承;2、不可变性(immutable)。除了hash这个属性其它属性都声明为final,因为它的不可变性,保证了线程安全性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响
-
StringBuffer就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,提供append和add方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上synchronized。但是保证了线程安全是需要性能的代价的。
-
StringBuilder是JDK1.5发布的,它和StringBuffer本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。
String 直接赋值和new String 有什么区别?
String s1="aaa";
String s2=new String("aaa");
System.out.println(s1.equals(s2)); -- true
System.out.println(s1==s2); -- false
String s1="aaa" 的操作:
- 在程序编译期,编译程序先去字符串常量池检查,是否存在“aaa”的常量,如果不存在,则在常量池中开辟一个内存空间存放“aaa”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为“s1”,存放的值为常量池中“aaa”的内存地址(相当于指向常量池中的“aaa”)。
String s2=new String("aaa")的操作:
- 在程序编译期,编译程序先去字符串常量池检查,是否存在“aaa”的常量,如果不存在,则在常量池中开辟一个内存空间存放“aaa”;如果存在的话,则不用重新开辟空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s2”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。
-
因为s1存放的是常量池aaa的内存地址,s2存放的是堆中String实例的内存地址,s1==s2 比较的是内存地址,所以为false
说说List,Set,Map的区别?
-
1.list接口存储一组不唯一且有序的对象,主要实现的接口类有ArrayList,LinkedList,CopyOnWriteArrayList,Vector
-
2.Set接口存储不允许重复的集合,不会有多个元素引用相同的对象,主要实现接口的类有:HashSet,LinkedHashSet,TreeSet,CopyOnWriteArraySet
-
3.Map 接口存储的是key,value键值对形式,Map会维护与key关联的值,两个key可以引用相同的对象,但是key不能重复,key可以是字符串,也可以是任意对象,有的key允许null值,有的不允许。实现Map 接口的主要类有:HashMap,LinkedHashMap,TreeMap,ConcurrentHashMap,ConCurrentSkipListMap,hashTable
说说ArrayList,LinkedList的区别,什么时候用ArrayList,什么时候用LinkedList?
- 1.ArrayList,LinkedList都是线程不安全的
- 2.ArrayList底层维护的是一个可变长度的Object数组,默认长度是10,LinkedList底层维护的是一个双向链表,默认长度是0
- 3.ArrayList,实现了RandomAccess接口,支持随机访问(get(int index)方法),
- 在add方法时,会判断是否需要进行扩容,如果需要,会调用grow() 方法,实际内部调用的是system.arrayCopy方法复制数组进行扩容,每次扩容是原来的1.5倍,如果有大量的插入操作,会频繁的进行扩容,影响效率,插入和删除是否受元素位置影响,因为每次指定插入数组都要往后移动一位,时间复杂度为 O(n-i)
- 4.LinkedList双向链表内,每个节点存放的是三个数据:直接前驱,存储数据,直接后继。添加元素时,直接添加到链表尾部,前驱指向上个节点,时间复杂度O(1),如果是要在指定位置i插入和删除元素的话(调用(add(int index, E element) 方法) 时间复杂度近似为o(n)),因为需要先移动到指定位置再插入。在有频繁插入数据的场景中,LinkedList要比ArrayList性能更高,如果是频繁读取,ArrayList要比LinkedList性能更高
- 5.内存占用,ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList和Vector有什么区别 ?
- ArrayList默认长度是10,Vector是0,Vector是线程安全的,在所有的操作方法上都加了synchronized,在单线程访问时也会加锁,性能很差,现在几乎不会使用vector,如果要保证线程安全,可以用CopyOnWriteArrayList
说下CopyOnWriteArrayList是如何保证线程安全的?
- CopyOnWriteArrayList 在add方法中,通过可重入锁ReentrantLock加锁,然后通过system.arraycopy方法将原来的数组进行复制,长度加1复制出一个新的数组,然后在新数组上执行写操作,结束之后再将原来数组的引用指向新的数组。CopyOnWriteArrayList适合读多写少的场景,get方法没有加锁,支持并发读。
- CopyOnWriteArrayList缺点:1.内存占有问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC 2.无法保证实时性, 因为写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。
hashMap 和hashtable 的区别?
- hashtable底层维护的是k,v的entry数组,默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。是线程安全的,key不允许为null值,在插入操作 put ,get都加了synchronized,单线程也需要获取锁,性能很慢,不能并发读。 HashTable 基本被淘汰,如果追求线程安全,可以用ConcurrentHashMap。
- hashMap 底层是entry 链表型数组,默认初始化长度是16,负载因子是0.75,负载因子是为了自动扩容的,在当数据量大于超过设定的阈值的时候(容量*负载因子),自动对map进行扩容,每次扩容,容量变为原来的2倍,默认初始容量-必须是2的幂,最大容量是2^30,是线程不安全的,key可以为null值,只有一个,可以多个value为null
能说下hashMap 底层数据结构么,是如何添加数据的?
- hashMap 底层维护的是一个链表型的数组,在put的时候,会将key 先做内部的hash处理,(h = key.hashCode()) ^ (h >>> 16); 通过key的hashCode与h右移16位后做异或运算,得到一个hash值,然后调用resize方法,判断是否是是第一次put,如果是,设置容量为默认16,如果不是,判断是否需要扩容,如果需要,扩容为原来的两倍,然后用table的长度-1和hash值做与运算(p = tab[i = (n - 1) & hash]),得到数组的下标,判断有没有值,如果没有,则新建个节点,把数据添加进去,如果有,判断这个key是不是同一个key,如果是直接覆盖,如果不是,并且p是一个红黑树,则直接添加到红黑树,如果不是红黑树,则代表是普通的链表节点,则先添加到当前节点尾部,然后看当前链表中的元素超过了TREEIFY_THRESHOLD所设置的数量时,默认是8个,就会触发树化,将其转化为红黑树,为什么要用红黑树呢?因为当大量的数据放入Map中,Hash冲突会越来越多,某些位置就会出现一个很长的链表的情况。这种情况下,查询时间复杂度是O(n) ,删除的时间复杂度也是O(n),查询、删除的效率会大大降低。而同样的数据情况下,平衡二叉树的时间复杂度都是O(logn)。
hashmap 每次扩容为什么是2的n次幂?,
-
-
第一是因为哈希函数的问题
通过除留余数法方式获取桶号,因为Hash表的大小始终为2的n次幂,因此可以将取模转为位运算操作,提高效率
-
第一是因为哈希函数的问题
-
- 可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低!
HashMap 和 hashSet的区别?
- hashSet 内部维护了一个hashmap,add插入方法是由hashmap来实现的,通过hashmap把值put到key中,value保存一个object对象,通过hashmap的key保证了唯一
HashMap 多线程操作导致死循环问题?
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环。主要发生在扩容的时候会调用resize()方法,resize()方法里有个transfer方法,这个方法会将每一个链表转化到新链表,而且链表中的位置发生反转,而这在多线程情况下是非常容易造成链表回路。从而在get()的时候陷入死循环。并发环境下推荐使用 ConcurrentHashMap 。
ConcurrentHashMap 和HashMap的区别?
- ConcurrentHashMap是线程安全的,HashMap是线程不安全的
- ConcurrentHashMap put的key不允许为null值,HashMap可以有一个null值
能说下ConcurrentHashMap底层是如何保证线程安全的,put操作是如何实现的?
- ConcurrentHashMap 底层还是 数组+链表+红黑二叉树实现的
- 实现线程安全的方式:1.在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
- 调用put的时候,会先计算key的hash值,然后进入一个无限循环(自旋锁),该无限循环可以确保成功插入数据,会先判断table表为空或者长度为0,满足条件会初始化table表
- 然后根据key的hash值取出table的节点元素,如果为空,则通过cas 的无锁方式放入数据,如果CAS成功,说明Node节点已经插入,随后addCount(1L, binCount)方法会检查当前容量是否需要进行扩容,如果CAS失败,说明有其它线程提前插入了节点,自旋重新尝试在这个位置插入节点
- 如果f的hash值为-1,说明当前f是ForwardingNode节点,意味有其它线程正在扩容,则一起进行扩容操作。扩容完下次循环再尝试插入
- 其余情况说明出现了hash冲突,把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用synchronized锁来实现的,会先将链表的第一个元素上锁,然后判断当前是链表还是红黑树,如果是链表,则会遍历链表,看是否能找到对应的node节点,如果能找到则修改value,否则在链表尾部加入节点。
- 如果f是TreeBin类型节点,说明f是红黑树根节点,会先设置 binCount = 2,然后通过自旋循环遍历树上节点元素,更新或增加节点。
- 如果链表中节点数binCount >= TREEIFY_THRESHOLD(默认是8),则把链表转化为红黑树结构。下个循环再插入元素
ConcurrentHashMap 1.8为什么多线程插入性能会很高?
- ConcurrentHashMap 内部通过 cas 操作,自旋锁操作,synchronized在链表头部加锁来保证线程安全,cas 操作本身是无锁的,没有锁竞争,synchronized在链表头部加锁也只发生在出现hash冲突的情况下,如果没有hash冲突,是没有加锁的,所有性能会很高 ,synchronized 1.6做了大量的锁优化
3^4 ,3 & 4 ,3 <<4 ,3 >>4 如何计算?
位运算,计算机对二进制数据进行的运算都叫位运算,主要包括一下几个:
符号 | 描述 | 运算规则 | 举例| |
---|---|---|---|
& | 与 | 两个位都为1时,结果才为1 | 3&4=0 |
| | 或 | 两个位都为0时,结果才为0 | 3|4=7 |
^ | 异或 | 两个位相同为0,相异为1 | 3^4=7 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 | 3 << 4=48 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) | 3 >> 4=0 |
异或运算可以交换两个数的值
集合应用中,有碰到过哪些坑么?
-
- 集合如果循环中要删除元素,要用迭代器遍历,或者反向遍历,不然遍历删除会报错:
//会报错
public static void main(String[] args) {
List<String> stringList = Lists.newArrayList("a", "b", "c");
for (String s : stringList) {
if (Objects.equals("a",s)) {
stringList.remove(s);
}
}
}
//不会报错
List<String> stringList = Lists.newArrayList("a", "b", "c");
for (int i = stringList.size() - 1; i >= 0; i--) {
String s = stringList.get(i);
if (Objects.equals("a", s)) {
stringList.remove(s);
}
}
//不会报错
public static void main(String[] args) {
List<String> stringList = Lists.newArrayList("a", "b", "c");
Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (Objects.equals("a", s)) {
iterator.remove();
}
}
System.out.println(stringList);
}
//不会报错
public static void main(String[] args) {
List<String> stringList = Lists.newArrayList("a", "b", "c");
stringList.removeIf(s -> Objects.equals("a", s));
System.out.println(stringList);
}
-
- Arrays.asList 方法返回的 List 不支持增加、删除操作,会报错,原因是Arrays.asList返回的list是一个内部类 arrayList,类并没有重写add方法,所以不支持该方法,一般用Lists.newArrayList(Arrays.asList(array))重新生成新的数组使用
//会报错
public static void main(String[] args) {
String[] array = new String[]{"a", "b", "c"};
List<String> stringList = Arrays.asList(array);
stringList.add("d");
System.out.println(stringList);
}
-
- Arrays.asList 操作set 时,会改写原来数组的值,导致不可变因素,原因是Arrays.asList返回的arrayList,set方法内部操作的也是当前数组,所以操作时set方法会改变原来数组结构
-
- 接口查询不到数据,默认返回空集合:Collections.emptyList() 添加元素会报错,因为emptyList返回的list定义final,不可变的集合,所以报错,最好接口需要返回空集合时用Lists.newArrayList
//会报错
public static void main(String[] args) {
List<String> stringList = Collections.emptyList();
stringList.add("aaa");
System.out.println(stringList);
}
-
- List.subList 方法操作修改,删除会更改原来集合的值,原因是subList返回的对象是ArrayList的内部类SubList,SubList 的add和remove方法调用的父类方法,也就是原来父类ArrayList的引用,所以会改变原始对象的值,使用的时候最好用新的list来替换。
public static void main(String[] args) {
List<String> list = Lists.newArrayList("a", "b", "c", "d", "e");
List<String> subList = list.subList(0, 3);
//subList 增加个值f
subList.add("f");
//输出list和 sublist ,会发现list的值也改变了
System.out.println("subList=="+subList.toString());
System.out.println("list=="+list.toString());
}
//输出结果,会发现subList 值改变后,list的值也变了
subList==[a, b, c, f]
list==[a, b, c, f, d, e]
public static void main(String[] args) {
List<String> list = Lists.newArrayList("a", "b", "c", "d", "e");
List<String> subList = list.subList(0, 3);
//用新的list来处理
subList = Lists.newArrayList(subList);
//subList 增加个值f
subList.add("f");
//输出list和 sublist ,会发现list的值也改变了
System.out.println("subList=="+subList.toString());
System.out.println("list=="+list.toString());
}