java面试

java面试知识库

1.java基础知识

覆盖常考点,有些太基础的没有写,核心考点都在这

1.String、StringBuffer、StringBuilder的区别

String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高

String是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer和StringBuilder都是在原对象上操作,不会生成新的对象
StringBuffer是线程安全的,StringBuilder线程不安全的
StringBuffer方法都是synchronized修饰的 性能:StringBuilder > StringBuffer > String
场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer

package demo;
public class Controller {
    public static void main(String[] args) {
      String s="abc";
     s="abcd";
      
    System.out.println(s);//abcd
/*源码
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */

        StringBuilder stringBuilder = new StringBuilder(s);
        stringBuilder.append("e");
        System.out.println("stringBuilder:"+stringBuilder);//stringBuilder:abcde
/*源码
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
 */

        StringBuffer stringBuffer = new StringBuffer(s);
        stringBuffer.append("f");
        System.out.println("stringBuffer:"+stringBuffer);//stringBuffer:abcdf
        /*源码
         public synchronized StringBuffer append(String str) {
                toStringCache = null;
                super.append(str);
                return this;
            }
 */ }}

2.==号和equals()方法都是比较是否相等的方法,那它们有什么区别和联系呢?

首先,号在比较基本数据类型时比较的是值,而用号比较两个对象时比较的是两个对象的地址值:

int x = 10;
int y = 10;
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(x == y); // true
System.out.println(str1 == str2); // false

那equals()方法呢?我们可以通过查看源码知道,equals()方法存在于Object类中,因为Object类是所有类的直接或间接父类,也就是说所有的类中的equals()方法都继承自Object类,而通过源码我们发现,Object类中equals()方法底层依赖的是号,那么,在所有没有重写equals()方法的类中,调用equals()方法其实和使用号的效果一样,也是比较的地址值,然而,Java提供的所有类中,绝大多数类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值:

String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1.equals(str2));//true
System.out.println(str1 == str2);//false
String s1 = "abc";
String s2 = "abc";
System.out.println(s1.equals(s2)); //true
System.out.println(s1 == s2);//true

为什么第二个会是true呢?
这就涉及到了内存中的常量池,常量池属于方法区的一部分,当运行到s1创建对象时,如果常量池中没有,就在常量池中创建一个对象"abc",第二次创建的时候,就直接使用,所以两次创建的对象其实是同一个对象,它们的地址值相等。

== 的作用:
  基本类型:比较的就是值是否相同
  引用类型:比较的就是地址值是否相同
equals 的作用:
  引用类型:默认情况下,比较的是地址值。
注:不过,我们可以根据情况自己重写该方法。一般重写都是自动生成,比较对象的属性值是否相同

==和equals比较
对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
首先,号在比较基本数据类型时比较的是值,而用号比较两个对象时比较的是两个对象的地址值:
equals:object中默认也是采用
比较,通常会重写

Object
public boolean equals(Object obj) {
 return (this == obj);
}

String
public boolean equals(Object anObject) { 
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
 int n = value.length;
if (n == anotherString.value.length) {
 char v1[] = value;
char v2[] = anotherString.value; 
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i]) 
return false;
i++;
}
return true;
}
}
return false;
} 

上述代码可以看出,String类中被复写的equals()方法其实是比较两个字符串的内容。
举例:
public class StringDemo {
public static void main(String args[]) { 
String str1 = "Hello";
String str2 = new String("Hello"); 
String str3 = str2; // 引用传递
System.out.println(str1 == str2); // false 
System.out.println(str1 == str3); // false 
System.out.println(str2 == str3); // true 
System.out.println(str1.equals(str2)); // true 
System.out.println(str1.equals(str3)); // true 
System.out.println(str2.equals(str3)); // true
}
}

3.JDK、JRE、JVM之间的区别

JDK:Java Develpment Kit java 开发工具
JRE:Java Runtime Environment java运行时环境
JVM:java Virtual Machine java 虚拟机

4.List和Set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素
Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐一遍历各个元素

5.ArrayList和LinkedList区别

ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会 涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚 至超过linkedList(需要创建大量的node对象)

 public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历,
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需 要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结 果为空时会遍历整个列表。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{//Deque  双端队列
public class Test01 {
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();//底层是数组实现
         arrayList.add("1");
         arrayList.add(3,"5");

        System.out.println("arrayList"+arrayList);

        LinkedList<String> linkedList = new LinkedList<>();//底层是链表实现
        linkedList.add("1");
        linkedList.add(3,"5");//先从头元素开始遍历到下表为3的,然后插入元素
        System.out.println("linkedList"+linkedList);
    }
}

说一下ArrayList和LinkedList区别
首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同
另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用

6.Java中的HashMap的工作原理是什么?

HashMap底层是Entry数组,是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合/从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。

7.HashMap的扩容机制原理

1.7版本
1.先生成新数组(以前的两倍长度)
2.遍历老数组中的每个位置上的链表上的每个元素
3.取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
4.将元素添加到新数组中去
5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本
1.先生成新数组(之前的数组两倍)
2.遍历老数组中的每个位置上的链表或红黑树
3.如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4.如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
a.统计每个下标位置的元素个数
b.如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
c.如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
5.所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

8. Jdk1.7到Jdk1.8 HashMap 发生了什么变化(底层)?

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
  2. 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
  3. 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

9.说一下HashMap的Put方法

先说HashMap的Put方法的大体流程:
根据Key通过哈希算法与与运算得出数组下标
如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位置
3.如果数组下标位置元素不为空,则要分情况讨论
如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
1).如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会 判断红黑树中是否存在当前key,如果存在则更新value
2).如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,那么则会将该链表转成红黑树
3).将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法

10.HashMap和HashTable有什么区别?其底层实现是什么?

区别 :
HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
HashMap允许key和value为null,而HashTable不允许
底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在
1.计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,
2.如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,
3.如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
4.key为null,存在下标0的位置

11.TreeMap和HashMap的区别

1、HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;
2、HashMap和TreeMap都不是线程安全的;
3、HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;
TreeMap继承SortedMap类;他保持键的有序顺序;
4、HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
5、HashMap:适用于Map插入,删除,定位元素;TreeMap:适用于按自然顺序或自定义顺序遍历键(key)

12.谈谈ConcurrentHashMap的扩容机制

1.7版本
1.7版本的ConcurrentHashMap是基于Segment分段实现的
每个Segment相对于一个小型的HashMap
3.每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
4.先生成新的数组,然后转移元素到新数组中
5.扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本
1.8版本的ConcurrentHashMap不再基于Segment实现
当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
3.如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
4.ConcurrentHashMap是支持多个线程同时扩容的
5.扩容之前也先生成一个新的数组(假设以前数组长度为4,那么经过扩容之后数组长度为8)
6.在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

13.ConcurrentHashMap原理,jdk7和jdk8版本的区别

jdk7:
数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁:Segment分段锁 Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segmentget方法无需加锁,volatile保证

jdk8:
数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被读线程感知

14.CopyOnWriteArrayList的底层原理是怎样的

1.首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
2.并且,写操作会加锁,防止出现并发写入丢失数据的问题
3.写操作结束之后会把原数组指向新数组
4.CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

15.Jdk1.7到Jdk1.8 java虚拟机发生了什么变化?

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体实现,之所以元空间所占的内存改成本地内存,官方的说法是为了和JRockit统一,不过额外还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区溢出,太大了又会占用了太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占用的内存

16.深拷贝和浅拷贝

就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值