Java面试题(一):Java基础
这几天准备系统性的整理一下这几年所学的知识,正好看到这份面试题了,https://mp.weixin.qq.com/s/IXXSqzaw561KqoO1xUorUw,比较精简,不像别的是在堆知识点,主要包含的内容了十九个模块:Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、Mybatis、RabbitMQ、Kafka、Zookeeper、MySql、Redis、JVM,
原文只有问题,没有回答,正好边回答问题,边整理知识点

一、Java 基础
1. JDK 和 JRE 有什么区别?
- JRE:JRE是Java Runtime Environment的缩写,顾名思义是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的,还有所有的Java类库的class文件,都在lib目录下,并且都打包成了jar。
至于在Windows上的虚拟机是哪个文件呢?就是<JRE安装目录>/bin/client中的jvm.dll。

- jdk:jdk是Java Development Kit的缩写,顾名思义是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。
总结:简单来说,JRE可以支撑Java程序的运行,包括JVM虚拟机(java.exe等)和基本的类库(rt.jar等),JDK可以支持Java程序的开发,包括编译器(javac.exe)、开发工具(javadoc.exe、jar.exe、keytool.exe、jconsole.exe)和更多的类库(如tools.jar)等。
2. == 和 equals 的区别是什么?
- “==” 比较的是两个引用在内存中指向的是不是同一内存空间,也就是说在内存空间中的存储位置是否一致。
- equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法,在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。
3. 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
4. final 在 java 中有什么作用?
- final可以修饰类,这样的类不能被继承。
- final可以修饰方法,这样的方法不能被重写。
- final可以修饰变量,这样的变量的值不能被修改,是常量。
5. java 中的 Math.round(-1.5) 等于多少?
Math.round()方法即为我们常说的“四舍五入”方法,原理是对传入的参数+0.5之后,再向下取整得到的数就是返回的结果,返回值为long型;
所以这里结果是-1
6. String 属于基础的数据类型吗?
java 中String 是个对象,是引用类型 ,基础类型与引用类型的区别是,基础类型只表示简单的字符或数字,引用类型可以是任何复杂的数据结构 ,基本类型仅表示简单的数据类型,引用类型可以表示复杂的数据类型,还可以操作这种数据类型的行为 。
java虚拟机处理基础类型与引用类型的方式是不一样的,对于基本类型,java虚拟机会为其分配数据类型实际占用的内存空间,而对于引用类型变量,他仅仅是一个指向堆区中某个实例的指针。
java中的基础类型一共有8个,它们分别为:
- 字符类型:byte,char
- 基本整型:short,int,long
- 浮点型:float,double
- 布尔类型:boolean
7. java 中操作字符串都有哪些类?它们之间有什么区别?
String、StringBuffer、StringBuilder
-
String是不可变的对象,对每次对String类型的改变时都会生成一个新的对象,StringBuffer和StringBuilder是可以改变对象的
-
对于操作效率:StringBuilder > StringBuffer > String
-
对于线程安全:StringBuffer 是线程安全,可用于多线程;StringBuilder 是非线程安全,用于单线程
-
不频繁的字符串操作使用 String。反之,StringBuffer 和 StringBuilder 都优于String
8. String str="i"与 String str=new String(“i”)一样吗?
不一样,因为他们不是同一个对象。
9. 如何将字符串反转?
- for循环
- new StringBuilder(“adsfadgstdyj”).reverse().toString()
- 集合反转Collections.reverse(list)
10. String 类的常用方法都有那些?
1、和长度有关:
- int length() 得到一个字符串的字符个数
2、和数组有关:
- byte[] getByte() ) 将一个字符串转换成字节数组
- char[] toCharArray() 将一个字符串转换成字符数组
- String split(String) 将一个字符串按照指定内容劈开
3、和判断有关:
- boolean equals() 判断两个字符串的内容是否一样
- boolean equalsIsIgnoreCase(String) 忽略太小写的比较两个字符串的内容是否一样
- boolean contains(String) 判断一个字符串里面是否包含指定的内容
- boolean startsWith(String) 判断一个字符串是否以指定的内容开头
- boolean endsWith(String) 判断一个字符串是否以指定的内容结尾
4、和改变内容有关:
- String toUpperCase() 将一个字符串全部转换成大写
- String toLowerCase() 将一个字符串全部转换成小写
- String replace(String,String) 将某个内容全部替换成指定内容
- String replaceAll(String,String) 将某个内容全部替换成指定内容,支持正则
- String repalceFirst(String,String) 将第一次出现的某个内容替换成指定的内容
- String substring(int) 从指定下标开始一直截取到字符串的最后
- String substring(int,int) 从下标x截取到下标y-1对应的元素
- String trim() 去除一个字符串的前后空格
5、和位置有关:
- char charAt(int) 得到指定下标位置对应的字符
- int indexOf(String) 得到指定内容第一次出现的下标
- int lastIndexOf(String) 得到指定内容最后一次出现的下标
11. 抽象类必须要有抽象方法吗?
抽象类中不一定要包含抽象(abstrace)方法,可以全部是普通方法,
即,抽象中可以没有抽象(abstract)方法。
但是,如果类中含有抽象方法,那么类必须声明为抽象类。
12. 普通类和抽象类有哪些区别?
- 抽象类不能被实例
- 抽象类不能有构造函数,抽象方法也不能被声明为静态
- 抽象类可以有抽象方法
- 抽象类的抽象方法必须被非抽象子类继承
13. 抽象类能使用 final 修饰吗?
不能,抽象类中的抽象方法是未来继承之后重写方法,而用final修饰的类,无法被继承。
14. 接口和抽象类有什么区别?
- 抽象类是被子类继承,接口是被类实现
- 接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
- 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量
- 接口是设计的结果 ,抽象类是重构的结果
15. java 中 IO 流分为几种?

总结:
-
1.Java IO是采用的是装饰模式,即采用处理流来包装节点流的方式,来达到代码通用性。
-
2.处理流和节点流的区分方法,节点流在新建时需要一个数据源(文件、网络)作为参数,而处理流需要一个节点流作为参数。
-
3.处理流的作用就是提高代码通用性,编写代码的便捷性,提高性能。
-
4.节点流都是对应抽象基类的实现类,它们都实现了抽象基类的基础读写方法。其中read()方法如果返回-1,代表已经读到数据源末尾。
16. Files的常用方法都有哪些?
1、创建:
- createNewFile()在指定位置创建一个空文件,成功就返回true,如果已存在就不创建,然后返回false。
- mkdir() 在指定位置创建一个单级文件夹。
- mkdirs() 在指定位置创建一个多级文件夹。
- renameTo(File dest)如果目标文件与源文件是在同一个路径下,那么* * * * renameTo的作用是重命名, 如果目标文件与源文件不是在同一个路径下,那么renameTo的作用就是剪切,而且还不能操作文件夹。
2、删除:
- delete() 删除文件或者一个空文件夹,不能删除非空文件夹,马上删除文件,返回一个布尔值。
- deleteOnExit()jvm退出时删除文件或者文件夹,用于删除临时文件,无返回值。
3、判断:
- exists() 文件或文件夹是否存在。
- isFile() 是否是一个文件,如果不存在,则始终为false。
- isDirectory() 是否是一个目录,如果不存在,则始终为false。
- isHidden() 是否是一个隐藏的文件或是否是隐藏的目录。
- isAbsolute() 测试此抽象路径名是否为绝对路径名。
4、获取:
- getName() 获取文件或文件夹的名称,不包含上级路径。
- getAbsolutePath()获取文件的绝对路径,与文件是否存在没关系
- length() 获取文件的大小(字节数),如果文件不存在则返回0L,如果是文件夹也返回0L。
- getParent() 返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回null。
- lastModified()获取最后一次被修改的时间。
5、文件夹相关:
- static File[] listRoots()列出所有的根目录(Window中就是所有系统的盘符)
- list() 返回目录下的文件或者目录名,包含隐藏文件。对于文件这样操作会返回null。
- listFiles() 返回目录下的文件或者目录对象(File类实例),包含隐藏文件。对于文件这样操作会返回null。
- list(FilenameFilter filter)返回指定当前目录中符合过滤条件的子文件或子目录。对于文件这样操作会返回null。
- listFiles(FilenameFilter filter)返回指定当前目录中符合过滤条件的子文件或子目录。对于文件这样操作会返回null。
二、容器(集合)
18. java 容器都有哪些?
|Collection
| ├List
| │-├LinkedList
| │-├ArrayList
| │-└Vector
| │ └Stack
| ├Set
| │├HashSet
| │├TreeSet
| │└LinkedSet
|
|Map
├Hashtable
├HashMap
└WeakHashMap
19. Collection 和 Collections 有什么区别?
- 1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
- 2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
20. List、Set、Map 之间的区别是什么?
List:
- 1.可以允许重复的对象。
- 2.可以插入多个null元素。
- 3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
- 4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set:
- 1.不允许重复对象
- 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
- 只允许一个 null 元素
- 4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
Map
- 1.Map不是collection的子接口或者实现类。Map是一个接口。
- 2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
- TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
- Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
- 5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)
21. HashMap 和 Hashtable 有什么区别?
第一、继承不同。
public class Hashtable extends Dictionary implements Map
public class HashMap extends AbstractMap implements Map
第二、Hashtable 中的方法是同步的(线程安全),而HashMap中的方法在缺省情况下是非同步的(线程不安全)。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。
第三、Hashtable中,key和value都不允许出现null值。在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
第四、两个遍历方式的内部实现上不同。Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
第五、哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
第六、Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
22. 如何决定使用 HashMap 还是 TreeMap?

- HashMap:适用于在Map中插入、删除和定位元素。
- Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
- HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap.
- HashMap: 非线程安全
- TreeMap: 非线程安全
- HashMap的结果是没有排序的,而TreeMap输出的结果是排好序的。
23. 说一下 HashMap 的实现原理?
HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好
24. 说一下 HashSet 的实现原理?
Set的实现类的集合对象中不能够有重复元素,HashSet也一样他是使用了一种标识来确定元素的不重复,HashSet用一种算法来保证HashSet中的元素是不重复的, HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75
Object类中的hashCode()的方法是所有子类都会继承这个方法,这个方法会用Hash算法算出一个Hash(哈希)码值返回,HashSet会用Hash码值去和数组长度取模, 模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。
HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成,我们应该为保存到HashSet中的对象覆盖hashCode()和equals(),因为再将对象加入到HashSet中时,会首先调用hashCode方法计算出对象的hash值,接着根据此hash值调用HashMap中的hash方法,得到的值& (length-1)得到该对象在hashMap的transient Entry[] table中的保存位置的索引,接着找到数组中该索引位置保存的对象,并调用equals方法比较这两个对象是否相等,如果相等则不添加,注意:所以要存入HashSet的集合对象中的自定义类必须覆盖hashCode(),equals()两个方法,才能保证集合中元素不重复。在覆盖equals()和hashCode()方法时, 要使相同对象的hashCode()方法返回相同值,覆盖equals()方法再判断其内容。为了保证效率,所以在覆盖hashCode()方法时, 也要尽量使不同对象尽量返回不同的Hash码值。
如果数组中的元素和要加入的对象的hashCode()返回了相同的Hash值(相同对象),才会用equals()方法来判断两个对象的内容是否相同。
25. ArrayList 和 LinkedList 的区别是什么?
1)LinkedList类
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意:LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:List list = Collections.synchronizedList(new LinkedList(…));
2) ArrayList类
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并 没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
和LinkedList一样,ArrayList也是非同步的(unsynchronized)。一般情况下使用这两个就可以了,因为非同步,所以效率比较高。
如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
26. 如何实现数组和 List 之间的转换?
/**
* List转数组:toArray(arraylist.size()方法
*/
int size=arrayList.size();
String[] a = arrayList.toArray(new String[size]);
/**
* 数组转list,Arrays的asList(a)方法
*/
List<String> list=Arrays.asList(a);
27. ArrayList 和 Vector 的区别是什么?
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的 Iterator是同一接口,但是,因为Vector是同步的,当一个 Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例 如,添加或删除了一些元素),这时调用Iterator的方法时将抛出 ConcurrentModificationException,因此必须捕获该 异常
28. Array 和 ArrayList 有何区别?
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
Array大小是固定的,ArrayList的大小是动态变化的。
ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。
ArrayList可以算是Array的加强版,(对array有所取舍的加强)。
另附分类比较:
存储内容比较:
Array数组可以包含基本类型和对象类型,
ArrayList却只能包含对象类型。
但是需要注意的是:Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为ArrayList可以存储Object。
空间大小比较:
它的空间大小是固定的,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。
ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大约0.5倍的新数组,然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部数组的空间是否足够。(比较麻烦的地方)。
附上arraylist扩充机制:newCapacity=oldCapacity+(oldCapacity>>1)(注: >>1:右移1位,相当于除以2,例如10>>1 得到的就是5)但由于源码里(不再分析,这里简要略过)传过来的minCapcatiy的值是size+1,能够实现grow方法调用就肯定是(size+1)>elementData.length的情况,所以size就是初始最大容量或上一次扩容后达到的最大容量,所以才会进行扩容。因此,扩容后的大小应该是原来的1.5倍+1
方法上的比较:
ArrayList作为Array的增强版,当然是在方法上比Array更多样化,比如添加全部addAll()、删除全部removeAll()、返回迭代器iterator()等。
适用场景:
如果想要保存一些在整个程序运行期间都会存在而且不变的数据,我们可以将它们放进一个全局数组里,但是如果我们单纯只是想要以数组的形式保存数据,而不对数据进行增加等操作,只是方便我们进行查找的话,那么,我们就选择ArrayList。而且还有一个地方是必须知道的,就是如果我们需要对元素进行频繁的移动或删除,或者是处理的是超大量的数据,那么,使用ArrayList就真的不是一个好的选择,因为它的效率很低,使用数组进行这样的动作就很麻烦,那么,我们可以考虑选择LinkedList。
29. 在 Queue 中 poll()和 remove()有什么区别?
- queue的增加元素方法add和offer的区别在于,add方法在队列满的情况下将选择抛异常的方法来表示队列已经满了,而offer方法通过返回false表示队列已经满了;在有限队列的情况,使用offer方法优于add方法;
- remove方法和poll方法都是删除队列的头元素,remove方法在队列为空的情况下将抛异常,而poll方法将返回null;
- element和peek方法都是返回队列的头元素,但是不删除头元素,区别在与element方法在队列为空的情况下,将抛异常,而peek方法将返回null
30. 哪些集合类是线程安全的?
早在jdk的1.1版本中,所有的集合都是线程安全的。但是在1.2以及之后的版本中就出现了一些线程不安全的集合,为什么版本升级会出现一些线程不安全的集合呢?因为线程不安全的集合普遍比线程安全的集合效率高的多。随着业务的发展,特别是在web应用中,为了提高用户体验减少用户的等待时间,页面响应速度(也就是效率)是优先考虑的。而且对线程不安全的集合加锁以后也能达到安全的效果(但是效率会低,因为会有锁的获取以及等待)。其实在jdk源码中相同效果的集合线程安全的比线程不安全的就多了一个同步机制,但是效率上却低了不止一点点,因为效率低,所以已经不太建议使用了。下面举一些常用的功能相同却线程安全和不安全的集合。
- Vector:就比Arraylist多了个同步化机制(线程安全)。
- Hashtable:就比Hashmap多了个线程安全。
- ConcurrentHashMap:是一种高效但是线程安全的集合。
- Stack:栈,也是线程安全的,继承于Vector。
31. 迭代器 Iterator 是什么?
迭代器(Iterator)是一个对象,它的工作是遍历并选择序列中的对象,它提供了一种访问一个容器(container)对象中的各个元素,而又不必暴露该对象内部细节的方法。通过迭代器,开发人员不需要了解容器底层的结构,就可以实现对容器的遍历。由于创建迭代器的代价小,因此迭代器通常被称为轻量级的容器
32. Iterator 怎么使用?有什么特点?
- 具有next和iter方法
- 通过一个next多次执行就可以获得所有这个容器中的值
- 迭代器中的值只能取一次
- 不取的时候值不出现
1)使用容器的iterator()方法返回一个Iterator,然后通过Iterator的next()方法返回第一个元素。
2)使用Iterator()的hasNext()方法判断容器中是否还有元素,如果有,可以使用next()方法获取下一个元素。
3)可以通过remove()方法删除迭代器返回的元素。
33. Iterator 和 ListIterator 有什么区别?
我们在使用List,Set的时候,为了实现对其数据的遍历,我们经常使用到了Iterator(迭代器)。使用迭代器,你不需要干涉其遍历的过程,只需要每次取出一个你想要的数据进行处理就可以了。
但是在使用的时候也是有不同的。List和Set都有iterator()来取得其迭代器。对List来说,你也可以通过listIterator()取得其迭代器,两种迭代器在有些时候是不能通用的,Iterator和ListIterator主要区别在以下方面:
-
ListIterator有add()方法,可以向List中添加对象,而Iterator不能
-
ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
-
ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
-
都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。
34. 怎么确保一个集合不能被修改?
unmodifiableSortedMap:返回指定有序映射的不可修改视图。此方法允许模块为用户提供对内部有序映射的“只读”访问。在返回的有序映射上执行的查询操作将“读完”指定的有序映射。试图修改返回的有序映射(无论是直接修改、通过其 collection 视图修改,还是通过其subMap、headMap或tailMap视图修改)将导致抛出UnsupportedOperationException。
如果指定的有序映射是可序列化的,则返回的有序映射也将是可序列化的。
public static void main(String[] args) {
Map<Integer,StringBuilder> map=new HashMap<Integer,StringBuilder>();
map.put(1,new StringBuilder("c1"));
map.put(2,new StringBuilder("c2"));
map.put(3,new StringBuilder("c3"));
Map<Integer,StringBuilder> unmodifiableMap=Collections.unmodifiableMap(map);
//这时候如果再往unmodifiableMap中添加元素,会发生错误
//unmodifiableMap.put(4,new StringBuilder("c4"));
unmodifiableMap.get(3).append("new");
System.out.println(unmodifiableMap.get(3));
}
35. 数组 线性链表 二叉树 哈希表
数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。
我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。
比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。存储位置 = f(关键字) 其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。
查找操作,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。
36.哈希冲突
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式,
三、对象拷贝
61. 为什么要使用克隆?
在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。在 Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现clone()方法是其中最简单,也是最高效的手段。
Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息
想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了。克隆分浅克隆和深克隆,浅克隆后的对象中非基本对象和原对象指向同一块内存,因此对这些非基本对象的修改会同时更改克隆前后的对象。深克隆可以实现完全的克隆,可以用反射的方式或序列化的方式实现。
62. 如何实现对象克隆?
- 浅克隆:实现Cloneable接口并重写Object类中的clone()方法;在重载方法中,调用super.clone()
- 深克隆:实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆;
这个是利用Serializable,利用序列化的方式来实现深复制(深克隆),在其中利用了Io流的方式将这个对象写到IO流里面,然后在从IO流里面读取,这样就实现了一个复制,然后实现序列化的这个会将引用的那个对象也一并进行深复制,这样就实现了这个机制,同时在IO里面读取数据的时候还使用了装饰者模式
63. 深拷贝和浅拷贝区别是什么?
浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚:对象A1中包含对B1的引用,B1中包含对C1的引用。浅拷贝A1得到A2,A2 中依然包含对B1的引用,B1中依然包含对C1的引用。深拷贝则是对浅拷贝的递归,深拷贝A1得到A2,A2中包含对B2(B1的copy)的引用,B2 中包含对C2(C1的copy)的引用。
- 浅克隆,在clone对象时,只会把基本数据类型的数据进行复制过去;如果是引用类型,只会把引用复制过去,也就是被克隆对象和原始对象信息,共同引用一个引用类型的属性。
- 深克隆:在克隆时,会把基本数据类型的数据和引用类型的数据,同时复制。克隆对象和原始对象不共同引用一个引用类型
缺点:在深克隆时,如果引用对象关系比较复杂,克隆时会很麻烦,因为每一个对象都要克隆。
解决方案:可以使用序列化进行解决。(先将对象写到流里,再 从流里读出来)
四、反射
57. 什么是反射?
Java Reflaction in Action有这么一句话,可以解释。反射是运行中的程序检查自己和软件运行环境的能力,它可以根据它发现的进行改变。通俗的讲就是反射可以在运行时根据指定的类名获得类的信息。
58. 什么是 java 序列化?什么情况下需要序列化?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化是为了解决在对对象流进行读写操作时所引发的问题。序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,
然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
-
1.概念
- 序列化:把Java对象转换为字节序列的过程。
- 反序列化:把字节序列恢复为Java对象的过程。
-
2.对象的序列化主要有两种用途:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 在网络上传送对象的字节序列。
-
3.注意事项
- 当一个父类实现序列化,子类自动实现序列化,不需要显式实现[Serializable接口]
- 当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
59. 动态代理是什么?有哪些应用?
- 动态代理:当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等。可以给这个类创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动态代理是一种在运行时动态地创建代理对象,动态地处理代理方法调用的机制。
实际上它是一种代理机制。代理可以看做是对调用目标的一个封装,直接通过代理来实现对目标代码的调用
-
动态代理实现:首先必须定义一个接口,还要有一个InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)。利用到InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
-
动态代理的应用:Spring的AOP(JDK动态代理和CGLIB字节码生成),加事务,加权限,加日志。
