熬夜整理了这么多年来的Java基础面试题,欢迎学习收藏,手机上可以点击这里,效果更佳https://mp.weixin.qq.com/s/ncbEQqQdJo0UaogQSgA0bQ
@w=400
Node:存放实际的key和value值。 sizeCtl:负数:表示进行初始化或者扩容,-1表示正在初始化,-N,表示有N-1个线程正在进行扩容 正数:0 表示还没有被初始化,>0的数,初始化或者是下一次进行扩容的阈值。 TreeNode:用在红黑树,表示树的节点, TreeBin是实际放在table数组中的,代表了这个红黑树的根。
HashMap容器O(1)的查找时间复杂度只是其理想的状态,而这种理想状态需要由java设计者去保证。
jdk1.7中的hashMap在最坏情况下,退化成链表后,get/put时间复杂度均为O(n);jdk1.8中,采用红黑树,复杂度可以到O(logN);如果hash函数设计的较好,元素均匀分布,可以达到理想的O(1)复杂度。
1). 数据结构不同:jdk7 数组+单链表; jdk8 数组+(单链表+红黑树) 。
JDK7 在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表。也就是说时间复杂度在最差情况下会退化到O(n)。 JDK8 如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销。也就是说put/get的操作的时间复杂度最差只有O(log n)。
2). 链表中元素位置不同:jdk7头插法;jdk8 链表尾插。
头插: 最近put的可能等下就被get,头插遍历到链表头就匹配到了,并发resize可能产生循环链。 尾插:保证了元素的顺序,并发resize过程中可能发生数据丢失的情况。
3). 扩容的处理不同:jdk7中使用hash和newCapacity计算元素在新数组中的位置;jdk8中利用新增的高位是否为1,来确定新元素的位置,因此元素要么在原位置,要么在原位置+扩容的大小值。
jkd7中,扩容时,直接判断每个元素在新数组中的位置,然后依次复制到新数组; jdk8中,扩容时,首先建立两个链表high和low,然后根据新增的高位是否为0,将元素放到对应的链表后面。最后将对应的链表放在原位置或者原位置+扩容大小值的位置.
从下图可以看到,是根据hash大小来确定左右子树的位置的。
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
if (!xp.red)
x.red = true;
else {
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
开放定址法(线性探测法,平方探测法,双散列)和再散列(选择新的散列函数,在另外一个大约两倍大的表,而且使用一个相关的新散列函数,扫描整个原始散列表,计算每个(未删除的)元素的新散列值并将其插入到新表中。)
1.7 hashmap 并发resize成环;1.8并发resize丢失数据。
1、取消了segment数组,引入了Node结构,直接用Node数组来保存数据,锁的粒度更小,减少并发冲突的概率。 2、存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个的时候。
jdk1.8中,resize数据要么在原位置,要么在原位置加上resize大小的位置。 concurrentHashMap在put或者remove操作时,发现正在进行扩容,会首先帮助扩容。
1.7 hashmap:新建new table,根据hash值计算在新table中的位置,依次移动到新table 1.8 hashmap:新建table,从旧table中复制元素,利用high和low来保存不同位置的元素。
1) 从上面的分析JDK8 resize的过程可以可能到,数组长度保持2的次幂,当resize的时候,为了通过h&(length-1)计算新的元素位置,可以看到当扩容后只有一位差异,也就是多出了最左位的1,这样计算 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致,否则index+OldCap。
2) 数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀。hash函数采用各种位运算也是为了使得低位更加散列,如果低位全部为1,那么对于h低位部分来说,任何一位的变化都会对结果产生影响,可以尽可能的使元素分布比较均匀。
ConcurrentHashMap。后者锁粒度更低,前者直接对put、get方法加锁。
HashSet的实现很简单,内部有一个HashMap的成员变量,所有的Set相关的操作都转换为了对HashMap的操作。
LinkedHashMap:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
...
}
可以看到,LinkedHashMap是HashMap的子类,但和HashMap的无序性不一样,LinkedHashMap通过维护一个运行于所有条目的双向链表,保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序,这个可以在初始化的时候确定,默认采用插入顺序来维持取出键值对的次序。
在成员变量上,与HashMap不同的是,引入了before和after两个变量来记录前后的元素。
在构造函数中,需要指定accessOrder,有两种情况:
false,所有的Entry按照插入的顺序排列 true,所有的Entry按照访问的顺序排列
ArrayList、LinkedList、Vector、stack的底层实现:
从图中我们可以看出:
TreeMap继承于AbstractMap,实现了Map, Cloneable, NavigableMap, Serializable接口。
@w=400
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。 TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
对于SortedMap来说,该类是TreeMap体系中的父接口,也是区别于HashMap体系最关键的一个接口。SortedMap接口中定义的第一个方法Comparator<? super K> comparator();
该方法决定了TreeMap体系的走向,有了比较器,就可以对插入的元素进行排序了。
TreeMap的查找、插入、更新元素等操作,主要是对红黑树的节点进行相应的更新,和数据结构中类似。
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型的好处:
ConcurrentSkipListMap是一个并发安全, 基于skiplist实现有序存储的Map。可以看成是TreeMap的并发版本。
下面的图示使用紫色的箭头画出了在一个SkipList中查找key值50的过程。简述如下:
查询复杂度:O(logN)
Redis 中的有序集合(zset) 支持的操作:
插入一个元素 删除一个元素 查找一个元素 有序输出所有元素 按照范围区间查找元素(比如查找值在 [100, 356] 之间的数据)
其中,前四个操作红黑树也可以完成,且时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。按照区间查找数据时,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,非常高效。
Java中的流是对字节序列的抽象,我们可以想象有一个水管,只不过现在流动在水管中的不再是水,而是字节序列。和水流一样,Java中的流也具有一个“流动的方向”,通常可以从中读入一个字节序列的对象被称为输入流;能够向其写入一个字节序列的对象被称为输出流。
Java中的字节流(Byte)处理的最基本单位为单个字节,它通常用来处理二进制数据。Java中最基本的两个字节流类是InputStream和OutputStream,它们分别代表了组基本的输入字节流和输出字节流。InputStream类与OutputStream类均为抽象类,我们在实际使用中通常使用Java类库中提供的它们的一系列子类。
public abstract int read() throws IOException;
Java中的字符流(Char)处理的最基本的单元是Unicode码元(大小2字节),它通常用来处理文本数据。所谓Unicode码元,也就是一个Unicode代码单元,范围是0x0000~0xFFFF。在以上范围内的每个数字都与一个字符相对应,Java中的String类型默认就把字符以Unicode规则编码而后存储在内存中。然而与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会有不同的二进制表示。实际上字符流是这样工作的:
字符流与字节流的区别
例如,Integer类型的变量能否==int类型变量,能否作比较
可以。 但是不同包装类型直接进行比较,不会发生自动拆箱,比较的是两个对象是否为同一个。
自动装箱和拆箱实现了基本数据类型与对象数据类型之间的隐式转换。
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型byte,short,char,int,long,float,double和boolean对应的封装类为Byte,Short,Character,Integer,Long,Float,Double,Boolean。
何时发生自动装箱和拆箱,
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
public static Integer show(Integer iParam){
System.out.println("autoboxing example - method invocation i: " + iParam);
return iParam;
}
//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer
自动装箱的弊端,
自动装箱有一个问题,那就是在一个循环中进行自动装箱操作的时候,如下面的例子就会创建多余的对象,影响程序的性能。
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}
自动装箱与比较:
下面程序的输出结果是什么?
public class Main {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
System.out.println(g.equals(a+h));
}
}
在解释具体的结果时,首先必须明白如下两点:
下面是程序的具体输出结果:
true
false
true
true
true
false
true
注意到对于Integer和Long,Java中,会对-128到127的对象进行缓存,当创建新的对象时,如果符合这个这个范围,并且已有存在的相同值的对象,则返回这个对象,否则创建新的Integer对象。
对于上面的结果:
cd:指向相同的缓存对象,所以返回true; ef:不存在缓存,是不同的对象,所以返回false; c(a+b):比较数值,因此为true; c.equals(a+b):比较的对象,由于存在缓存,所以两个对象一样,返回true; g(a+b):直接比较数值,因此为true; g.equals(a+b):比较对象,由于equals也不会进行类型转换,a+b为Integer,g为Long,因此为false; g.equals(a+h):和上面不一样,a+h时,a会进行类型转换,转成Long,接着比较两个对象,由于Long存在缓存,所以两个对象一致,返回true。
关于equals和==:
.equals(...)
will only compare what it is written to compare, no more, no less.equals(Object o)
method of the closest parent class that has overridden this method.Object#equals(Object o)
method. Per the Object API this is the same as ==; that is, it returns true if and only if both variables refer to the same object, if their references are one and the same. Thus you will be testing for object equality and not functional equality.以下是Effective Java的建议:
You must override hashCode() in every class that overrides equals(). Failure to do so will result in a violation of the general contract for Object.hashCode(), which will prevent your class from functioning properly in conjunction with all hash-based collections, including HashMap, HashSet, and Hashtable.
主要是考虑到在map等需要hashcode的场合,保证程序运行正常。
由于Object是所有类的基类,默认的equals函数如下,直接比较两个对象是否为同一个。
public boolean equals(Object obj) {
return (this == obj);
}
默认的hashcode方法是一个native函数,
public native int hashCode();
It just means that the method is implemented in the native aka C/C++ parts of the JVM. This means you can't find Java source code for that method. But there is still some code somewhere within the JVM that gets invoked whenever you call hashCode() on some Object.
从Object中关于hashCode方法的描述,对于不同的Object,hashCode会返回不同的值;但是实现可能与对象的地址有关,也有可能无关,具体看JVM实现。
String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。
StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
序列化: 对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。
反序列化: 客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
序列化算法一般会按步骤做如下事情:
JDK中序列化一个对象:
反序列化 将一个InputStream封装在ObjectInputStream内,然后调用readObject()。最后获得的是一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。
序列化的控制——通过实现Externalizable接口——代替实现Serializable接口——来对序列化过程进行控制。
防止对象的敏感部分被序列化,两种方式:
Externalizable的替代方法
java8
java9
java10
//之前的代码格式
URL url = new URL("http://www.oracle.com/");
URLConnection conn = url.openConnection();
Reader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))
//java10中用var来声明变量
var url = new URL("http://www.oracle.com/");
var conn = url.openConnection();
var reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
var是一个保留类型名称,而不是关键字。所以之前使用var作为变量、方法名、包名的都没问题,但是如果作为类或接口名,那么这个类和接口就必须重命名了。
var的使用场景主要有以下四种:
java11
首先解释下"类对象"的概念:在Java中,类是是对具有一组相同特征或行为的实例的抽象并进行描述,对象则是此类所描述的特征或行为的具体实例。作为概念层次的类,其本身也具有某些共同的特性,如都具有类名称、由类加载器去加载,都具有包,具有父类,属性和方法等。于是,Java中有专门定义了一个类,Class,去描述其他类所具有的这些特性,因此,从此角度去看,类本身也都是属于Class类的对象。为与经常意义上的对象相区分,在此称之为"类对象"。
在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 『 = 』号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。
而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。
1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。(默认的clone函数)
2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。(序列化对象或者重写clone函数)
接口和抽象类是 Java 面向对象设计的两个基础机制。
接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义 Java 标准类库中,定义了非常多的接口,比如 java.util.List。
Java 8 以后,接口也是可以有方法实现的。 从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。在 Java 8 中添加了一系列 default method,主要是增加 Lambda(forEach)、Stream 相关的功能。
抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。 Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。
Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词,可以参考 Java 标准库中的 ArrayList。
1. JDK动态代理
1、因为利用JDKProxy生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有。 2、生成的代理类的所有的方法都拦截了目标类的所有的方法。而拦截器中invoke方法的内容正好就是代理类的各个方法的组成体。 3、利用JDKProxy方式必须有接口的存在。 4、invoke方法中的三个参数可以访问目标类的被调用方法的API、被调用方法的参数、被调用方法的返回类型。
2. cglib动态代理
1、 CGlib是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。 2、 用CGlib生成代理类是目标类的子类。 3、 用CGlib生成 代理类不需要接口 4、 用CGLib生成的代理类重写了父类的各个方法。 5、 拦截器中的intercept方法内容正好就是代理类中的方法体
JDK动态代理和cglib动态代理有什么区别?
Spring框架的一大特点就是AOP,SpringAOP的本质就是动态代理,那么Spring到底使用的是JDK代理,还是cglib代理呢? 答:混合使用。如果被代理对象实现了接口,就优先使用JDK代理,如果没有实现接口,就用用cglib代理。
在Java程序中,不区分传值调用和传引用,总是采用传值调用
。
注意以下几种情况:
值传递与引用传递的区别:一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值,这句话相当重要,这是按值调用与引用调用的根本区别。
就算是包装类型也不行,修改之后生成新的变量,改变了形参的值,但是实参的值不会改变。
由于String类和包装类都被设定成不可变的,没有提供value对应的setter方法,而且很多都是final的,我们无法改变其内容,所以导致我们看起来好像是值传递。
在Java下实现swap函数可以通过反射实现,或者使用数组。
会有问题,不过需要分情况讨论。
所有可能的删除方法如下,
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("aa");
list.add("bb");
list.add("bb");
list.add("aa");
list.add("cc");
// 删除元素 bb
remove(list, "bb");
for (String str : list) {
System.out.println(str);
}
}
public static void remove(ArrayList<String> list, String elem) {
// 五种不同的循环及删除方法
// 方法一:普通for循环正序删除,删除过程中元素向左移动,不能删除重复的元素
// for (int i = 0; i < list.size(); i++) {
// if (list.get(i).equals(elem)) {
// list.remove(list.get(i));
// }
// }
// 方法二:普通for循环倒序删除,删除过程中元素向左移动,可以删除重复的元素
// for (int i = list.size() - 1; i >= 0; i--) {
// if (list.get(i).equals(elem)) {
// list.remove(list.get(i));
// }
// }
// 方法三:增强for循环删除,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
// for (String str : list) {
// if (str.equals(elem)) {
// list.remove(str);
// }
// }
// 方法四:迭代器,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
// Iterator iterator = list.iterator();
// while (iterator.hasNext()) {
// if(iterator.next().equals(elem)) {
// list.remove(iterator.next());
// }
// }
// 方法五:迭代器,使用迭代器的remove()方法删除,可以删除重复的元素,但不推荐
// Iterator iterator = list.iterator();
// while (iterator.hasNext()) {
// if(iterator.next().equals(elem)) {
// iterator.remove();
// }
// }
}
}
针对上述结果总结如下:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。
Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
它们的区别在于创建对象的方式不一样,前者newInstance()是使用类加载机制,后者new关键字是创建一个新类。那么为什么会有两种创建对象方式?这主要考虑到软件的可伸缩、可扩展和可重用等软件设计思想。
1、类的加载方式不同
在执行Class.forName("a.class.Name")时,JVM会在classapth中去找对应的类并加载,这时JVM会执行该类的静态代码段。在使用newInstance()方法的时候,必须保证这个类已经加载并且已经链接了,而这可以通过Class的静态方法forName()来完成的。 使用关键字new创建一个类的时候,这个类可以没有被加载,一般也不需要该类在classpath中设定,但可能需要通过classlaoder来加载。
2、所调用的构造方法不尽相同
new关键字能调用任何构造方法。 newInstance()只能调用无参构造方法。
3、执行效率不同
new关键字是强类型的,效率相对较高。 newInstance()是弱类型的,效率相对较低。
既然使用newInstance()构造对象的地方通过new关键字也可以创建对象,为什么又会使用newInstance()来创建对象呢?
假设定义了一个接口Door,开始的时候是用木门的,定义为一个类WoodenDoor,在程序里就要这样写 Door door = new WoodenDoor() 。假设后来生活条件提高,换为自动门了,定义一个类AutoDoor,这时程序就要改写为 Door door = new AutoDoor() 。虽然只是改个标识符,如果这样的语句特别多,改动还是挺大的。于是出现了工厂模式,所有Door的实例都由DoorFactory提供,这时换一种门的时候,只需要把工厂的生产模式改一下,还是要改一点代码。
而如果使用newInstance(),则可以在不改变代码的情况下,换为另外一种Door。具体方法是把Door的具体实现类的类名放到配置文件中,通过newInstance()生成实例。这样,改变另外一种Door的时候,只改配置文件就可以了。示例代码如下: String className = 从配置文件读取Door的具体实现类的类名; Door door = (Door) Class.forName(className).newInstance();
再配合依赖注入的方法,就提高了软件的可伸缩性、可扩展性。
https://blog.csdn.net/luckykapok918/article/details/50186797
MAP:
不安全:hashmap、treeMap、LinkedHashMap 安全:concurrentHashMap、ConcurrentSkipListMap、或者说hashtable
List:
不安全:ArrayList、linkedlist 安全:Vector、SynchronizedList(将list转为线程安全,全部上锁)、CopyOnWriteArrayList(读读不上锁、写上锁ReentrantLock、写完直接替换)
Set:
不安全:hashset、treeSet、LinkedHashSet 安全:ConcurrentSkipListSet、CopyOnWriteArraySet、synchronizedSet
反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射主要提供以下功能:
重点:是运行时而不是编译时。
反射最重要的用途就是开发各种通用框架。 很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
1. 获得Class对象
方法有三种:
(1) 使用 Class 类的 forName 静态方法:
public static Class<?> forName(String className)
比如在 JDBC 开发中常用此方法加载数据库驱动,Class.forName(driver);
(2) 直接获取某一个对象的 class,比如:
Class<?> klass = int.class;
Class<?> classInt = Integer.TYPE;
(3) 调用某个对象的 getClass() 方法,比如:
StringBuilder str = new StringBuilder("123");
Class<?> klass = str.getClass();
2. 判断是否为某个类的实例
一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:
public native boolean isInstance(Object obj);
3. 创建实例
通过反射来生成对象主要有两种方式。
Class<?> c = String.class;
Object str = c.newInstance();
//获取String所对应的Class对象
Class<?> c = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c.getConstructor(String.class);
//根据构造器创建实例
Object obj = constructor.newInstance("23333");
System.out.println(obj);
4. 获取方法
获取某个Class对象的方法集合,主要有以下几个方法:
public Method[] getDeclaredMethods() throws SecurityException
public Method[] getMethods() throws SecurityException
public Method getMethod(String name, Class<?>... parameterTypes)
5. 获取构造器信息
获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:
public T newInstance(Object ... initargs)
此方法可以根据传入的参数来调用对应的Constructor创建对象实例。
6、获取类的成员变量(字段)信息
主要是这几个方法,在此不再赘述:
注:可以通过method.setAccessible(true)和field.setAccessible(true)来关闭安全检查来提升反射速度。
7. 调用方法
当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
下面是例子:
public class test1 {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> klass = methodClass.class;
//创建methodClass的实例
Object obj = klass.newInstance();
//获取methodClass类的add方法
Method method = klass.getMethod("add",int.class,int.class);
//调用method对应的方法 => add(1,4)
Object result = method.invoke(obj,1,4);
System.out.println(result);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}
8. 利用反射创建数组
数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:
public static void testArray() throws ClassNotFoundException {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls,25);
//往数组里添加内容
Array.set(array,0,"hello");
Array.set(array,1,"Java");
Array.set(array,2,"fuck");
Array.set(array,3,"Scala");
Array.set(array,4,"Clojure");
//获取某一项的内容
System.out.println(Array.get(array,3));
}
其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
Static :被static修饰的成员变量属于类,不属于这个类的某个对象。
final意味着”不可改变的”,一般应用于数据、方法和类。
如何让类不被继承:用final修饰这个类,或者将构造函数声明为私有。
内存溢出:是指程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError。
产生该错误的原因主要包括:
内存泄露:Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点: 1)首先,这些对象是可达的,即在有向图中,存在通路可以与其相连; 2)其次,这些对象是无用的,即程序以后不会再使用这些对象。
两者的联系:
内存泄露会最终会导致内存溢出。
相同点:都会导致应用程序运行出现问题,性能下降或挂起。 不同点:
在 Java 中重载是由静态类型确定的,在类加载的时候即可确定,属于静态分派;而重写是由动态类型确定,是在运行时确定的,属于动态分派,动态分派是由虚方法表实现的,虚方法表中存在着各个方法的实际入口地址,如若父类中某个方法子类没有重写,则父类与子类的方法表中的方法地址相同,如若重写了,则子类方法表的地址指向重写后的地址。
这样就完成的实现了Lambda表达式,使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口,内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法。
当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。
当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。 比如当我们在new一个类的实例的时候,如果在运行是类找不到,则会抛出一个NoClassDefFoundError的错误。