java 基础面试题
1.int 和integer的区别?
1.Integer是int 的包装类,int是java的一种基本数据类型;
2.Integer必须实例化才能使用,而int不需要;
3.Integer实际上是对象的引用,当new一个Integer时,实际是生成一个指针指向对象,而int是直接存储对象的值;
4.Integr默认值是null,而int默认值是0.
5.对于两个非new生成Integer对象进行比较时,如果两个变量在-127到128之间,则比较结果为true;
不在这个区间范围则为fasle;因为Integer i=100;会翻译成Integer.valueoOf(100);在-127到128之间的数会进行缓存,下次再次相同的数时,直接从缓存中获取。
自动装箱和自动拆箱?
自动装箱:可以将基础数据类型包装成对应的包装类;Integer a=10000,编译器会改为new Integer(10000);
自动拆箱:将包装类型转化为基础类型。int i=new Integer(1000) 编译器会修改为 int i=new Integer(10000).intValue()
2.List和Set的区别?
list和set均继承自collection。List是有序可以重复的,set是无序的不可以重复的。
3.StringBuilder和StringBuffer的区别?
1.StringBuilder是线程不安全的,而StringBuffer是线程安全的,因为所有的StringBuffer的公开方法都是用synchronzied修饰的。
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
2.缓冲区
StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
3.性能
既然 StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder 的性能要远大于 StringBuffer。
4.==和equals区别?
==:基本数据类型之间的比较是直接比较值得大小;引用数据类型是比较在内存中的地址,即堆内存中的地址。
equals:继承自Object类,默认比较在堆内存中的地址,在一些类库中该方法被重写了,如String、Integer在这些方法有自身的实现,直接比较的就是值是否相等。如果希望比较的对象的值是否相同,则需要重写equals方法。
5.String为啥是不可变的?你对String不变性的了解?
1.String类是被final修饰的,不能被继承。如果不是被final修饰,那么就有子类继承它,并且修改String的数值;
2.在用+号链接时字符串时会创建新的字符串,实际上底层是通过StringBuilder实例的append方法来实现;
3.在开发中我们常用String来表示url/文件路径,如果String可变的,或存在安全隐患;
6.String a=new String("aaa")创建了几个对象?
一个或两个.
aaa创建之后就会放入到常量池中(下次创建String时先查找常量池,有需要就使用,没有就去创建并存入常量池中),如果常量池有的话,就创建一个new 的对象,首先在堆内存中先创建一个空白的对象,并将a初始化,然后将常量池中的查找的值拷贝一份放入到堆对应的位置。
7.重写equals方法不重写hashCode方法带来的问题?
为什么要重写hashCode,一般情况下,我们使用的数据结构都是数组、list等等,但是存在一个问题,当我们查询数组里边是否存在元素X时,此时只能挨个遍历数组元素吗,时间复杂度是O(n);如果我们通过hash来查询某个元素,直接通过计算hashCode值来确定存放的位置。
例如向Set方法中
HashSet set = new HashSet<>();
Person p1 = new Person("张三", 18);
Person p2 = new Person("张三", 18);
set.add(p1);
set.add(p2);
System.out.println(set.size());
此时会有两个元素,明显是不合理的。重写了equals方法,不重写hashCode方法时,可能会出现equals方法返回为true,而hashCode方法却返回false。这样的一个后果会导致在hashmap、hashSet等类中存储多个一模一样的对象。
8.hashCode与equals的区别?
equals方法要求满足: 自返性:a.equals(a)返回true
传递性:a.equals(b) 返回true b.equals(c)返回true 则a.equals(c) 返回true
对称性:a.equals(c)返回true,则 c.equals(a)返回true 一致性:a.equals(c) 返回true 多次调用结果是一致的。
hashCode:他是一个本地方法
两个对象equals相同,则hashCode也相同;
两个对象的hashCode相同,则equals不一定相同;
hashCode不同时,则equals也不同;
举例子:
向hash结构添加数据:集合首先会调用hashCode方法,这样就可以直接定位他的位置,若该位置存在元素,则直接保存,若该处有元素,调用equals方法来匹配这两个元素是否相同,相同则不存,不同则在该元素的下一个位置存储元素。
先调用 hashCode,唯一则存储,不唯一则再调用 equals,结果相同则不再存储,结果不同则散列到其他位置。因为 hashCode 效率更高(仅为一个 int 值),比较起来更快。
9.为啥不可以重写一个java.lang.String类?
因为加载某个类时优先使用父类加载器加载需要使用的类,自定义的Stirng类使用的加载器是AppClassLoader,根据优先使用父类加载器的原理,AppClassLoader的父类是ExtClassLoader,所以这时加载的String使用的是ExtClassLoader,但是在jre/lib/ext 目录下没有找到String.Class类,然后继续交由其父类BootStrapClassLoader加载,在rt.jar找到了String.Class类,将其加载在内存中。这就是类加载器的委托机制。
10.抽象类和接口的区别?
1.抽象类中的方法可以不是抽象的,但是接口中的方法必须都是抽象的;
2.抽象类只能单继承,接口可以继承多个父接口;
3.抽象类中可以有普通的成员变量;接口中的变量,必须被初始化 , 接口中只有常量,没有变量
11.面向对象的三大特性?
封装:将数据和操作数据的方法绑定起来,隐藏内部实现细节,对外提供接口;
继承:子类继承父类中的方法或者属性,或者添加自己独有的特性,代码重用,可扩展性。
多态:允许不同子类对象可以对同一个消息做出不同响应。
12.Iterator / ListIterator / Iterable
对于普通for循环不能删除元素,否则会抛出异常。iterator可以边循环元素边删除元素。
Collection继承自Iterator接口,List继承自Collection接口,所以可以使用迭代器和forEach。iterator接口定义的方法:hasNext():判断游标右边是否有数据;next():返回游标右边的元素并将游标移动到下一个位置;
13.foreach的局限性
增强for循环在编译时转换为for循环,数组会被修改为下标式的循环;集合会被修改为iterator循环。
不适用于:
对Collection或者数组不能做添加和删除操作;
遍历过程中,collection 或数组中同时只有一个元素可见,即只有“当前遍历到的元素”可见,而前一个或后一个元素是不可见的;
14.Comparable 与 Comparator区别?
Comparable是内部比较器。排序的实体类都实现了Comparable接口,重写compareTo方法。基本数据类型包装类和String类都实现了Comparable接口,实现了Comparable接口的类的对象的类的列表或者数组通过Collections.sort() 进行自动排序。
Comparator外部比较器。如果实现类没有实现Comparable接口,又想进行两个类的比较,或者实现了Comparable接口,但是比较的方法不满足自己的需求,可以通过Comparator实现。
15.NIO /BIO /AIO区别?
Bio:同步阻塞, 在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作完成,用户进程才能继续执行。
NIO:同步非阻塞,在此种方式下,用户进程在发起一个IO操作之后可以直接做其他的事情,但是用户进程需要时不时的询问IO操作是否就绪,这就需要用户进程不停询问,从而引入了不必要的CPU资源。
AIO:异步非阻塞,用户进程只需要发起一个IO操作后立即返回,等IO操作真正的完成后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO操作。
16.ArrayList与LinkedList区别?
ArrayList和LinkedList都实现了List接口。
ArrayList:底层是数组,非线程安全。
ArrayList访问随机元素时间复杂度为O(1),插入或删除操作需要大量移动元素,效率较低;
只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量,每次扩容为原来的1.5倍;
可以存储null值;
ArrayList每次修改时,都需要进行修改自身的modCount;在生成迭代器时,会保存该modCount值,迭代器每次获取元素时,会比较自身的modCount与ArrayList的modCount是否相等,来判断容器是否修改,如果被修改则抛出异常。
LinkedList:底层是基于双向链表机制实现的,非线程安全的;
在插入元素时,须创建一个新的Entry对象,并切换相应的元素的前后元素的引用;
在查找元素时,须遍历链表;删除元素时,遍历链表,找到要删除的元素,然后从该位置删除该元素;
LinkedList的查询的时间复杂度是O(n),添加的时间复杂度是O(1);
16.1.数组的长度是有限制的,而ArrayList是可以存放任意数量对象,长度不受限制,那么他是怎么实现的呢?
他就是通过数组扩容的方式去实现的。
比如我们新增一个新元素,发现已经满了,首先他会定义一个容量为15的数组,然后把原数组的数据原封不动的复制到新数组中,这时候把指向原数据的地址换到新数组。
17.快速失败和安全失败区别?
快速失败和安全失败是对迭代器而言的。
快速失败:在使用迭代器遍历一个集合对象时,如果遍历的过程中对集合对象的内容进行了修改,则会抛出ConcurrentModificationException;
集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改。
安全失败:
在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。CopyOnWriteArrayList,CopyOnWriteArraySet,
18.hashmap原理是什么?
18.1 什么是hashmap?
hashmap是基于map接口的非同步实现,线程不安全,是为了快速存取而设计的;
采用key-value进行存放元素,允许null键和null值,只允许一个键为null,允许多个value为null;
jdk7及之前的版本,hashmap的数据结构是数组+链表,在链表中插入元素时采用的是头插法。
在jdk8及以后的版本,采用的是数组+链表+红黑树,也就是说hashmap的底层采用的是数组实现的,数组的每个位置都存储一个单向链表,当链表的长度大于八时,就会转成红黑树,在链表中插入元素的形式是尾插法。
18.2 hashmap的put()方法添加元素的过程?
1.计算元素存放的位置,拿到key的hashcode值后,调用hash方法重新计算hash值,JDK8 及之后的版本,对 hash() 方法进行了优化,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是即使 table 数组的长度较小,在计算元素存储位置时,也能让高位也参与运算。
2.将key-value放到数组中:如果计算的位置出现空,那么直接将这个元素插入到该位置中;
如果该位置已经存在链表,则使用equals比较链表是否存在key相同的节点,如果为true,则替换原元素;如果不存在,则在链表尾部插入节点。
如果插入元素后,如果链表的节点数是否超过8个,超过则转化为红黑树节点。
最后判断hashmap总容量是否超过阈值,则调用resize方法进行扩容,扩容之后数组的长速度变为原来的两倍。
18.3 jdk1.7 之前采用的是头插法,为啥1.8改成尾插法?
所谓头插法就是将新来的数据会取代原来的位置数据,将原来的数据推到链表中去,因为这样认为后插入的值可能被查到的可能性会大些。
但是jdk8使用的尾插法,主要是为了避免在扩容时发生死循环。
- 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
- ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
举个例子,我们现在往一个容量大小为2的put两个值,负载因子是0.75,我们在put第二个的时候就会进行resize,
2*0.75 = 1 所以插入第二个就要resize了。当我们要通过不同线程来插入元素5和7,此时两个线程A和B执行put()操作,A线程先执行,执行完transfer () 中的Entry<K,V> next = e.next;被挂起,此时e指针指向5,next指向7,如图所示。
此时线程B执行,将数组扩大两倍,链表仍散列在下标为1处,由于使用了头插法,节点位置将会交换,如图所示。
这时候线程A又执行,因为原先e指向5,next执行7,继续执行下一条语句e.next = newTable[i];,这时会出现5指向7的情况,如图所示。
19.hashmap为什么是线程不安全的?
jdk7——》在多线程环境下,扩容时会造成死循环或数据丢失;
造成数据丢失是因为使用的是头插法,新加入的节点会从头节点加入,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入到新的头结点之后,B也写入头结点,那么B的操作会覆盖A的写入操作,造成数据的丢失。
jdk8——》在多线程环境下,会发生数据覆盖的情况。
20.hashmap扩容机制?
1.当数组中元素个数(size)超过临界值threshold = capacity * loadfactor重新建立一个新的数组,长度原数组的两倍;
2.遍历旧数组的每个数据,重新计算每个元素在新数组的储存位置。
3.将旧数组的每个数据使用的尾插法逐个转移到新数组中,并重新设置扩容阈值。
21.hashmap与hashtable的区别?
1.继承的父类:两者都实现了map接口,但是hashmap继承自AbstractMap接口,而hashtable继承自Dictionary类。
2.使用方式:前者允许键和值都为null,但是键只允许一个键为null;后者不允许键和值为null;
3.数据结构:前者使用的数组+链表+红黑树,后者使用的数组+链表;
4.元素的hash值:HashMap的hash值是重新计算过的,Hashtable直接使用Object的hashCode;
5.初始容量及扩容方式:HashMap 的默认初始容量为16,每次扩容为原来的2倍;Hashtable 默认初始容量为11,每次扩容为原来的2倍+1;
22.线程安全的hashmap有哪些?
1.hashtable:它底层的每个方法都使用了 synchronized 保证线程同步,所以每次都锁住整张表,在性能方面会相对比较低。
2.使用Collections.synchronizedMap()方法来获取一个线程安全的集合,底层原理是使用synchronized来保证线程同步。
3.使用 ConcurrentHashMap集合。
23.ConcurrentHashMap原理?
jdk1.7:
在jdk1.7中,本质上还是采用"Segment数组+HashEntry数组+链表"形式存储键值对的;
但是为了提高并发,把原来的整个table划分为n个segment,从整体来看,它是由segment组成的数组,然后每个Segment里边有HashEntry组成的数组;
segment类继承自ReentrantLock类,所以也能充当锁的角色,因为锁住的是每个segment,从而避免每次put操作都得锁住整个数组。在默认情况下,可以允许16个线程并发无阻塞地操作集合对象。
为了保证可见性,能够实时读到主内存中的值,在ConcurrentHashMap中,大量使用 Unsafe中的方法,这类方法是利用一个CAS算法实现无锁化的修改值操作,可以大大减少使用加锁造成的性能消耗。
jdk1.8:
在jdk1.8中,本质上采用的是数组+链表+红黑树,采用的是synchronized+Cas算法来保证安全的。是延迟初始化的,在插入数据时,才会被初始化大小为2的n次方的(默认是16)Node数组。当链表的长度大于等于8就直接转换为红黑树TreeBin。
24.ConcurrentHashMap 的初始化流程?
1.根据并发级别计算ssize,创建数量为ssize的segment;
2.根据initialCapacity / ssize 计算出cap,初始化s0,S0作为原型对象去创建对应的Segment,同时创建数量为cap的hashEntry;
25.ConcurrentHashMap 的put流程?
jdk1.7:
1.对key值进行hash算法,计算出元素具体分到哪个segment;
2.再通过hash值计算出它在对应的segment的HashEntry数组的下标,使用头插法进行插入元素,在插入元素之前先加锁,这时其他的线程通过计算也是放到这个segment,那么就需要获取锁;
3.如果在插入元素之前,检查到这次操作会超过segment的阈值,就会对该segment进行扩容操作,然后再插入。
4.最后释放该segment的锁。
jdk1.8:
1.首先不允许key和value为null的情况放入。对于每个放入的值,首先利用spread方法进行对key的hashcode进行一次hash计算,来确定这个值在table中的位置;
2.如果数组为空,初始化大小为2的n次方的(默认是16)Node数组。
3.如果这个位置为空,则直接放入,通过cas操作进行插入数据,无需加锁。
4.如果这个位置不为空但是moved节点,表示正在扩容,需要线程线程来帮忙进行扩容;
5.如果这个位置不为空并且是链表节点,先对桶中的头节点进行加锁,遍历节点,如果key值是一样的,则只需要进行更新value值。否则遍历到结尾将节点插入到尾部。如果加入节点后链表长度大于8,则进行转换红黑树。如果是树节点,直接调用插入树节点的方法进行插入。
6.最后调用addCount()方法,就是把整个 table 的元素个数加 1;修改值大致流程是:当需要修改元素数量时,线程会先去 CAS 修改(元素个数) volatile 修饰baseCount 加1,若成功即返回。若失败,则线程被分配到某个 CounterCell ,然后操作 value 加1。若成功,则返回。否则,给当前线程重新分配一个 CounterCell,再尝试给 value 加1。
26.ConcurrentHashMap 的扩容流程?
jdk1.7:
1.扩容segment下的容量为之前的两倍,同时更新阈值;
2.计算每个元素的最新下标值,如果原来链表只有一个数据,直接把这个节点放到新表的位置;
3.如果原来链表不只有一个数据,这时需要进行判断是否需要批量移动,也就是从末尾开始到之前的某个位置,这些数据计算的下标都是一样的,那么就直接将这些数据一次性的移动到新表中;
4.移动lastRun之前的数据到新表中。
jdk1.8:
1.创建两倍长的数组nextTable。
2.将原来 table 中的元素迁移到 nextTable 中。(元素扩容迁移的时候,所有线程都遵循从后向前推进的规则,数组的长度为8,A线程先选择下标6/7,每个线程会确定一个固定范围,假如为2,那么B线程向前推进只能选择4.5进行迁移。同时维护一个全局变量transferIndex ,每个线程迁移时需要修改这个transferIndex为当前推进之后的下标,当transferIndex=0,说明迁移完成。)
2.1(1)初始化 ForwardingNode 对象,充当占位节点,hash 值为 -1,该占位对象存在时表示集合正在扩容状态。
- 占位作用,用于标识数组该位置的桶已经迁移完毕
- 作为一个转发的作用,扩容期间如果遇到查询操作,遇到转发节点,会把该查询操作转发到新的数组上去,不会阻塞查询操作。
3.如果当前桶没有元素,则直接通过 CAS 放置一个 ForwardingNode 占位对象,以便查询操作的转发和标识当前位置已经被处理过。
如果线程遍历到节点的 hash 值为 MOVE,也就是 -1(即 ForwardingNode 节点),则直接跳过,继续处理下一个桶中的节点
如果不是这两种情况则直接给当前桶节点加上 synchronized 锁,然后重新计算该桶的元素在新数组中的应该存放的位置,并进行数据迁移。
4.当前桶位置的数据迁移完成后,将 ForwardingNode 占位符对象设置到当前桶位置上,表示该位置已经被处理了
27.反射机制是什么?
java反射机制是程序在运行时加载类并且获取类的详细信息,从而操作类和对象的属性和方法。
反射原理:
1.当程序创建一个student对象时,会触发jvm加载该student的class文件。
2.JVM从磁盘中找到student的class文件加载到内存中;
3.class文件加载在内存中,jvm会自动创建一个class对象,一个类只会产生一个class对象。
如何获取class对象:
Class<UserInfo> userInfoClass= (Class<UserInfo>) userEntity.getClass();
Class<UserInfo> userInfoClass1=UserInfo.class;
Class<?> aClass = Class.forName("com.sq.docker.entity.UserInfo");
如何通过反射创建对象:
class.newInstance();
先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建对象,这种方法可以用指定的构造器构造类的实例。
反射优缺点:
优点:可以在程序的运行期间获得类的信息并操作一个类中的方法;
缺点:反射的代码可读性和维护性都比较低;
反射的代码执行的性能低;
反射破坏了封装性。
反射能够破坏单例模式;
例如:
public class Singleton3 {
private static volatile Singleton3 singleton3;
private Singleton3(){}
public static Singleton3 getInstance(){
if(singleton3==null){
synchronized (Singleton3.class){
if(singleton3==null){
singleton3=new Singleton3();
}
}
}
return singleton3;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton3 singleton3=Singleton3.getInstance() ;
Singleton3 singleton4=Singleton3.getInstance() ;
System.out.println(singleton3==singleton4);
Constructor<Singleton3> constructor=Singleton3.class.getDeclaredConstructor();
// 使用反射对象时可以取消访问限制检查
constructor.setAccessible(true);
Singleton3 singleton31=constructor.newInstance();
System.out.println(singleton31==singleton3);
}
}
28.深拷贝和浅拷贝的区别?
实现该Cloneable接口都会具备被拷贝的能力,同时拷贝在内存中进行,在性能方面比我们直接通过new生成的对象来的快。拷贝分为深拷贝和浅拷贝:
浅拷贝:java中对象的克隆,为了获取对象的一份拷贝,可以利用实现object的clone方法,拷贝规则是: (1)如果变量时基本类型,则拷贝其值,比如int或者float;
(2)如果变量是一个实例对象,则拷贝其地址引用,也就是说此时新对象与原来对象时共用该实例变量。
所以说浅拷贝只是Java提供的一种简单的拷贝机制,不便于直接使用。
深拷贝:深拷贝要把复制的对象引用也复制一遍;利用序列化来实现深拷贝,把对象写到流里的过程是序列化的过程。
29.值传递和引用传递的区别?
值传递:在调用函数时,将传递参数复制一份到到函数中,这样在函数中对参数进行修改,不会影响到实际参数;
引用传递:在调用函数时,将传递参数的地址传递到函数中,那么在函数中对参数进行修改,将会影响到实际参数。
\