1. String类为什么是final的。
答:申明为final的类是不能被继承的,这防止了String类被子类修改。由于String类是被设计为Immutable的,也就是不可变的,用final修饰,能防止String类被子类修改成可变的。
*题外话:*
关于String不可变的原因,收集了以下几个点:
1.由于String常量池的设计,当需要创建String时,会先从常量池中找是否已经存在该值,如果有则返回其引用,而不是创建一个相同值的String对象。如果String可变,那改变一个值会将所有引用这个值的变量都改了。
2.String大量作为参数传递,如果String值可变,那在方法内部就可以改变String的值,这会非常麻烦,以及可能造成安全上的问题。
3.Because immutable objects can not be changed, they can be shared among multiple threads freely. This eliminates the requirements of doing synchronization.
关于如何创建不可变的类的规则:
1.immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。
2.Immutable类的所有的属性都应该是final的。
3.对象必须被正确的创建,比如:对象引用在对象创建过程中不能泄露(leak)。
4.类应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。
5.如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)
(final关键字可以参考Java编程思想140,java核心技术卷一160)
2. Java集合类:list、set、queue、map实现类
HashSet, linkedHashSet,HashMap,linkedHashMap:都是基于散列表实现的,即链表数组。
LinkedHashMap继承了HashMap,并在node上多加了两个指针,来维护每个node之间的顺序,原有的hashmap的结构是没变的。
HashSet只是封装下HashMap,实际就是一个hashMap,只是这个map只能对键进行操作,set里的元素被当做是Map的key来存的,map的value存的是一个static final new object(),循环这个set其实就是取得HashMap的keySet来循环。
LinkedHashSet继承了HashSet,调用的是HashSet里构造linkedHashMap的构造方法,实际就是个linkedHashMap,然后只能对map的key进行操作。
TreeSet,TreeMap: 都是基于红黑树实现的,特点是所得到的结果是经过排序的,次序由元素实现的Comparable或者Comparator决定。将元素加到TreeSet比HashSet慢,不过TreeSet是排序的。
PriorityQueue:是接口Queue的实现。 使用堆数据结构,堆是一个可以自我调整的二叉树,对树执行添加和删除操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。优先级队列可以按任意的顺序插入元素,却总是按照排序的顺序进行检索,也就是无论何时调用remove方法,总会获得当前优先级队列中最小的元素(即优先级最高的元素,习惯上将1设为最高优先级)。默认是按元素的自然顺序排序,如存储Integer,char,String类型的元素,也可以通过自定义Comparator来排序。但是与TreeSet不同,如果仅迭代这些元素,是不会对元素进行排序的,PriorityQueue可以确保调用peek,poll,remove方法时,获取的元素是队列中优先级最高的元素。
DelayQueue:无界阻塞队列,用于放置实现了Delayed接口的对象,其中对象只能在到期时才能从队列中取出。DelayQueue其实就是在每次往优先级队列中添加元素,然后以元素的delay/过期值作为排序的因素,以此来达到先过期的元素会拍在队首,每次从队列里取出来都是最先要过期的元素。
(集合相关参考java编程思想第17章,java核心技术卷一第十三章)
3. ArrayList和LinkedList各自实现和区别
ArrayList是采用的是数组形式来保存对象的,这种方式将对象放在连续的位置中,所以最大的缺点就是插入删除时很慢,因为每次操作都需要移动其他元素的位置。优点是随机访问很快,可以通过查看该集合是否实现了RandomAccess接口来检测一个特定的集合是否支持高效的随机访问。
LinkedList是采用链表来存储数据,链表将对象存储在独立的节点中,每个节点还存放着序列中下一个节点的引用。链表增删快,但是查询慢,每次查询需要迭代整个列表。
4. HashMap的源码,实现原理,底层结构。
Hashmap底层是以链表数组实现的,数组并不保存键本身,也不直接保存值,而是保存key和value节点的链表。原理是通过键对象生成一个数字,将其作为数组的下标,这个数字就是散列码(哈希值)(哈希值还需对map的size取余,才是最终下标,保证计算出来的下标都在size内)。也就是key和Value存储在key的散列码对应的那个数组下标的链表里面。由于不同的key可以产生相同的散列码,这个冲突由链表解决,如果key的equals()也相同,那直接替换oldVaue;如果不相同,那将value add到链表的最后。取值也是一样,先查询key的散列码对应的链表元素是否存在,存在就循环链表里每一个元素,然后取出与key相同(equals())的那个元素。可以尝试用LinkedList实现一个简单的hashmap。源码里是用的Node[] table。
(hashmap相关参考java编程思想483~495,java核心技术卷一576~598)
5. 如何自己实现一个Map,描述
可以直接用LinkedList数组来存储,list里存包含键值对的Entry对象。put方法存放数据,get方法取数据。还需要一个entrySet方法可以遍历整个map。
Put方法的基本思路是:先根据key的hash值与size取余来确定该key-value在数组中存放的位置。如果数组中这个位置没有元素,那直接new一个entry对象,存到LinkedList里,再把linkedList设置到数组这个位置上。如果数组中已有元素,将这个linkedList取出来,循环list并将新的key与list里所有key比较,如果找到相同的key,直接覆盖value。如果没有相同的key,那也new一个entry对象加到这个linkedlist末尾。
Get方法跟put方法用相同的算法查找key,即根据key的hash值与size取余来确定该key-value在数组中存放的位置,没有就返回null。有就循环比较是否有相同的key,有就取出对应的value。
EntrySet方法主要是要返回一个有迭代器的集合,可以遍历,所以可以直接new一个hashSet,然后遍历整个链表数组,将每个entry都加到HashSet里。
6. Hash冲突怎么办?哪些解决散列冲突的方法,rehash?
以下内容网上查的,都能查到就不贴过来了。个人觉得得看点书理解下,推荐看下算法导论-11章 散列表。现在hashMap里是用的链表处理冲突。
一般解决hash冲突有四种方式:开放定址法,再哈希,链接法,建立公共溢出区。
7. HashMap冲突很厉害,最差性能,你会怎么解决?
HashMap使用链接法解决冲突,最差性能就是所有元素全部存放在同一个地址,形成一个单链表,这样查找、删除元素性能就是O(n)。可以考虑在节点过多时将单链表转成红黑树,双向链表删除元素是O(1),红黑树查找性能是O(logN),jdk1.8的hashmap已经实现了。
8. Hashtable,HashMap,ConcurrentHashMap 底层实现原理与线程安全问题
HashTable是用同步关键字保证线程安全,在线程竞争激烈的情况下,效率非常低。如线程1使用put方法添加元素,线程2不但不能使用put添加元素,也不能使用get获取元素。
HashMap是非线程安全的。在多线程环境下使用put操作会引起死循环,导致CPU利用率接近100%,因为多线程会导致HashMap的Entry链表形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
JDK1.7中ConcurrentHashMap实现:使用分段锁技术控制并发访问,首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。因此当多个线程访问不同数据段的数据时,线程间不存在锁竞争,从而有效提高并发访问效率。
JDK1.7里ConcurrentHashMap数据结构:之前了解过HashMap是以链表数组存储的,数组下标表示通过Key的hash值计算得到位置,key和value就存在该数组位置的Entry链表里,一个Entry节点包含key,value,和next节点。
ConcurrentHashMap是由Segment数组和Entry数组构成(Entry数组可以理解成一个HashMap)。Segment是一种可重入锁(ReetrantLock),在ConcurrentHashMap里扮演锁的角色。Entry用于存键值对数据。一个ConcurrentHashMap包含一个Segment数组,segment的结构和HashMap类似,是链表数组结构。一个segment包含一个Entry数组,每个Entry是一个链表结构的元素。每个Segment守护着一个Entry数组里的元素,当对Entry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
下图来自网络:
9. 反射中,Class.forName和classloader的区别
Class.forName默认加载并初始化class,即会执行静态代码块和初始化静态变量。
ClassLoader默认只加载类进JVM,并不初始化。
如JDBC链接,就需要使用Class.forName,因为需要执行静态代码块注册驱动到DriverManager上,之后才能通过DriverManager获取连接。
Class.forName("com.mysql.jdbc.Driver");//通过这种方式将驱动注册到驱动管理器上。
题外话:
Proxy.newProxyInstance()是在类的字节码文件不存在,也就是没有xx.class文件的时候,先根据该类的接口创建一个代理类的实例即xx.class文件,然后通过classLoader加载进JVM,最后根据JVM里的字节码文件创建实例并返回。
Class.forName()是在类的实例存在即XX.class文件存在,调用该方法让JVM将该文件加载进JVM,成为JVM里的一份字节码文件,也就是Class对象。不同于ClassLoader的是,Class.forName默认会初始化类,也就是load进JVM后,还会执行类里的静态方法和静态成员变量的赋值。
ClassLoader,也是在类的实例存在即XX.class文件存在,调用该方法让JVM将该文件加载进JVM,成为JVM里的一份字节码文件,也就是class对象。
(类加载机制,参考深入理解Java虚拟机第7章)
10. Java7、Java8的新特性
Java7:(只关注过这两个。。)
1.switch中可以使用字串了
2.运用List tempList = new ArrayList<>(); 即泛型实例化类型自动推断
Java8:
1.Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中。
2.方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
3.默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。
4.Stream API −新添加的Stream API(java.util.stream)把真正的函数式编程风格引入到Java中。
5.Date Time API − 加强对日期与时间的处理。
6.Optional 类 − Optional 类已经成为Java 8 类库的一部分,用来解决空指针异常。
Lambda,方法引用,Stream具体语法可参考:
https://blog.csdn.net/jinzhencs/article/details/50748202
11. JDK1.8 ConcurrentHashMap原理及实现
Jdk1.8里已经不用分段锁了,没有了segment数组。整体实现其实和HashMap是差不多的,只是加了线程安全相关的代码,结合了Synchronized、volitale关键字,以及CAS的方式,去实现线程安全。
此外HashMap在1.8中数据结构有改动,如下图2-4描述:
12. string、stringbuilder、stringbuffer区别
String对象是不可变的,String类里每一个看起来能修改String值的方法,实际上都创建了一个新的String对象,用来包含修改后的值,原始的String对象并没有改变。
由于String对象不可变,用+拼接字符串,会产生一大堆需要垃圾回收的中间对象,可以简单理解为每调用一次+就生成一个新的String对象。
StringBuilder的append方法拼接字符串,只会生成一个StringBuilder对象,效率更高。
StringBuffer是线程安全的,方法上都加了同步关键字,开销更大。
13 .异常的结构,运行时异常和非运行时异常,各举个例子
Throwable类是java中所有异常的超类,有Exception和Error两个子类。
Error是程序无法处理的错误,如OutOfMemoryError,stackOverFlowError,NoClassDefFoundError。
Exception是程序本身可以处理的异常。RuntimeException是Exception的一个子类,其子类如ArrayIndexOutOfBoundsException,NullPointerException,ClassCastException等。非运行时异常如IOException,FileNotFoundException。
14. String 类的常用方法
Length(),replace(),split(),indexOf(),CharAt(),trim(),toUpperCase(),subString(),equals().....
15. Java 的引用类型有哪几种
强引用,软引用,弱引用,虚引用。
强引用:直接new一个对像并赋值给一个变量,这个变量就是对这个对象的一个强引用。只要这个强引用没被释放,那么这个对象就不会被回收。
软引用:用SoftReference包裹的对象,如果一个对象只存在一个软引用,在内存溢出之前,将会被回收。软引用主要用于实现内存敏感的高速缓存。当内存不够用时,sf对象会被回收,sf.get()会返回null。
弱引用:用WeakReference包裹的对象,如果一个对象只存在一个弱引用,在垃圾回收时这个对象会被回收。WeakHashMap就是用的WeakReference来保存对象。WeakReference的Entry继承了WeakReference类。
虚引用: Phantomreference,垃圾回收时回收,永远无法通过引用取到对象值。
pf.get();//永远返回null
pf.isEnQueued();//返回对象是否已被回收
虚引用主要用于检测对象是否已经从内存中删除,可以通过这个通知机制来做额外的清场工作。 因此有些情况可以用PhantomReference 代替finalize(),做资源释放更明智。
(引用相关参考java编程思想518,深入理解JVM虚拟机65页,博客资料)
16. 抽象类和接口的区别
一般说来抽象类都包含一个或多个抽象方法,此外还可以包含具体数据和具体方法。不过也可以将没有包含抽象方法的类申明为抽象类,抽象类不能被实例化。
Jdk1.8之前接口只能有空方法,jdk1.8里接口可以有多个default的具体方法。
一个类可以实现多个接口,但是只能继承一个超类。
17. java的基础类型和字节大小。
Int: 4字节(32位,最大值2^31-1)
byte:1 字节(8位)
short:2 字节(16位)
Long:8 字节(64位)
double:8 字节(64位)
float:4字节(32位)
boolean:1位
char:2 字节(16位)
18. hashCode() 与 equals() 生成算法、方法怎么重写
Jdk1.7中,hashCode的定义可以调用Objects.hash(),可传入多个需要散列的属性作为参数,hashcode方法可以简单的写成:Objects.hash(name, id);
Equals就是必须和hashCode的定义一致,如果x.equals(y),那么x和y的hashcode必须相同。也就是定义equals和hashcode的域要相同。
(hashcode和equals参考Java核心技术卷一169-175)
19. Java类加载器包括几种?他们之间的父子关系是怎么样的?双亲委派机制是什么意思?有什么好处?
启动类加载器:加载java_home/lib下面jvm识别的jar
扩展类加载器:加载java_home/lib/ext下面的jar
应用程序类加载器:加载用户类路径也就是classpath上的jar
自定义加载器:
双亲委派模型:先让父类加载,如果没有父类,就到顶层的启动类加载器,如果都没有加载出来,那再自己去加载。
(参考深入理解JVM虚拟机第7章)
20.讲讲NIO
BIO就是同步阻塞式IO,当一个客户端向server端发起请求,必须等到Server端把这个请求处理完成,才能接收到server端的返回消息。同时,如果有其他请求发送到server端,server端是不会对新请求做出响应的,必须等到之前的请求处理完。这就是同步阻塞式。
NIO是同步非阻塞式IO,NIO面向Channel和Buffer,而不是传统的IO流。客户端发送的连接请求被作为一个channel注册到Selector上,Selector不断轮询各个channel的状态,只有在channel状态是可连接或者可读或者可写的时候,才会进行IO操作。因此NIO不用开启一个线程等待读或者写数据,而BIO则会一直等。
21. String编码UTF-8 和GBK的区别?
GBK包含全部中文字符;UTF-8则包含全世界所有国家需要用到的字符。GBK编码专门用来解决中文编码的,是双字节的。不论中英文都是双字节的。UTF-8 编码是用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文使用24位(三个字节)来编码。
所以如果是UTF-8,英文是1字节,中文是三字节。
GBK,英文中文都是2字节。
22. 什么时候使用字节流、什么时候使用字符流?
使用字符流的应用场景:如果是读写数据的时候需要转换成字符,比如处理文字则使用字符流。
使用字节流的应用场景:如果读写的数据都不需要转换成字符的时候,比如处理图片则使用字节流。