目录

[toc]

Java 异常体系

Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:

  • ErrorError类对象由 Java 虚拟机生成并抛出(如内存溢出。堆栈溢出),大多数错误与代码编写者所执行的操作无关。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,不应该试图去处理它所引起的异常状况。
  • Exception
    • 运行时异常RuntimeExdeption,如空指针、越界、算术异常或用户自定义的异常等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
    • 非运行时异常 :除RuntimeExdeption之外的异常,如IOException,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。

不受检查异常:包括RuntimeException及其子类和Error,无法预测,在程序执行过程中才可能发生。

检查异常:在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理(try… catch…/throw 进行处理)。

怎么获取 class 对象

Java 提供了四种方式获取 Class 对象: 1. 知道具体类的情况下可以使用:

Class alunbarClass = TargetObject.class;

但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化 2. 通过 Class.forName()传入类的全路径获取:

Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");

3. 通过对象实例instance.getClass()获取:

TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();

4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:

ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");

通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行

反射是什么,有什么优缺点,有哪些应用场景

反射是指在程序运行时动态地获取类的信息并操作类的属性、方法、构造函数等的能力。通过反射,可以在程序运行时动态地加载、使用、修改、生成类。Java 反射机制主要包含了以下三个类:Class 类、Constructor 类和 Method 类。

反射的优点

  • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
  • 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
  • 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。

反射的缺点

尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。

  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。

  • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。

  • 内部暴露 【重要】 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

  • Trail: The Reflection API

  • 深入解析 Java 反射(1)- 基础

反射的应用

动态代理

利用 JDK 的动态代理,可以不需要对每个代理目标都编写一个对应的代理类(代理类也需要实现代理目标实现的接口,并传入一个代理目标作为成员变量),InvocationHandler 可以利用反射自动为传入的对象生成代理类。

为什么不直接用继承+重写的方法来进行功能增强呢?因为可能别人写的程序我们没有源代码,只有 class 文件,无法重写。

动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler. invoke)。这样,在代理目标接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
// 代理:需要实现InvocationHandler接口
public class ProxyHandler implements InvocationHandler {
    /**
     * 绑定委托对象,并返回代理类
     */
    private Object target;
	
    // 绑定代理目标并为其动态生成一个代理对象返回
    public Object bind(Object target) {
        //绑定该类实现的所有接口,取得代理类
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
	
    // 在调用自动生成的代理对象的方法是会自动调用本方法,可以实现不对代理目标的方法进行重写的前提下进行方法功能增强(如加入日志打印。权限控制等)!
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 直接调用对象的实际方法,此处也可做一些调用权限控制
        Object result = null;
        if ("destroy".equals(method.getName())) {
            throw new IllegalAccessError();
        }
        result = method.invoke(target, args);
        return result;
    }
}
 
// 代理目标:必须实现某个接口!
public class RealSubject implements Subject {
    @Override
    public void doOperation() {
        System.out.println("RealSubject: do operation()...");
    }
 
    @Override
    public void destroy() {
        System.out.println("RealSubject: destroy()...");
    }
}

HashMap、TreeMap、LinkedHashMap

接口 java. util. Map, 主要有四个常用的实现类,分别是 HashMap、Hashtable、LinkedHashMap 和 TreeMap,类继承关系如下图所示:

map structure

下面针对各个实现类的特点做一些说明:

1. HashMap

它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。
HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,
可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,
或者使用 ConcurrentHashMap。

2. Hashtable

Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,
任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。
Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

3. LinkedHashMap

LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

4. TreeMap

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的 Comparator,否则会在运行时抛出 java. lang. ClassCastException 类型的异常。

对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。 通过上面的比较,我们知道了HashMap是Java的Map家族中一个普通成员,鉴于它可以满足大多数场景的使用条件,所以是使用频度最高的一个。下文我们主要结合源码,从存储结构、常用方法分析、扩容等方面了解一下HashMap的工作原理。

HashMap 的原理

HashMap 是基于哈希表存储的,在 JDK1.6,JDK1.7 版本采用数组 (桶位) + 链表实现存储元素和解决冲突,同一 hash 值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。但是到 JDK1.8 版本时 HashMap 采用位桶 + 链表 + 红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。 HashMap 内部是基于一个数组来实现的,数组中的每个元素称为一个桶 (bucket)。当数组中被占用的桶的数量超过了装载因子和数组容量设定的阈值后,会对数组进行扩容,容量将扩展为原来的 2 倍。哈希表中所有的 Entry 会被重新散列到新的位置中。
因为两个不同的 key 在散列时有可能发生冲突,HashMap 为了避免哈希冲突带来的影响做了几点优化。在进行散列处理时,将高位与低位进行异或,从而减小冲突的概率。 当不同的 node 被散列到同一个桶中时,每个桶中使用单向链表的方式来保存数据。在 Java 8 的实现中,如果一个桶中的 Node 数量超过了阈值 (TREEIFY_THRESHOLD = 8),就会将单链表转化为红黑树,当低于阈值 (UNTREEIFY_THRESHOLD = 6) 时重新转化为单链表。
分析了 HashMap 的 resize 方法可以知道,HashMap 在进行扩容时是非常耗性能的操作,所以在使用 HashMap 的时候,应该先估算一下 map 的大小,初始化的时候给一个大致的数值,避免 map 进行频繁的扩容。

HashMap 默认初始容量,怎么扩容的,为什么,指定大小后初始容量又是多少

HashMap 的默认初始容量是 16,即在创建一个新的 HashMap 对象时,如果没有指定初始容量,那么它的初始容量就是 16。 HashMap 在添加元素时,会先计算元素的哈希值,然后根据这个哈希值找到元素应该放置的桶,如果该桶已经存在元素,那么就将新元素放在链表的头部。当链表长度达到一定程度时(默认为8),就会将链表转化为红黑树,这样可以提高查找效率。如果桶的数量不够用了,就会对HashMap进行扩容,扩容时会将容量翻倍,并且将原来的元素重新计算哈希值放入新的桶中。 HashMap进行扩容的原因是为了减少哈希冲突,提高HashMap的性能。扩容操作需要重新计算哈希值和重新分配桶,因此会带来一定的性能开销,但是这样可以使HashMap保持在一个较低的负载因子(默认为0.75),保证查询和插入元素的时间复杂度都是O(1)。 当我们手动指定 HashMap 的初始容量时,HashMap 会将这个容量设置为大于等于指定容量的最小的 2 的幂。 例如,如果我们手动指定 HashMap 的初始容量为 20,那么实际的初始容量就是 32(大于等于 20 的最小的 2 的幂)。这样可以提高 HashMap 的性能,因为在 HashMap 内部使用二进制位操作来计算哈希值,如果容量是 2 的幂次方,那么哈希值的计算会更加高效。

HashMap put 方法的细节、put 操作和扩容的先后顺序;

LinkedHashMap

参考:搞懂 Java LinkedHashMap 源码 - 掘金 (juejin.cn)

图片很直接的说明了一个问题,那就是 LinkedHashMap 直接继承自 HashMap ,这也就说明了上文中我们说到的 HashMap 一切重要的概念 LinkedHashMap 都是拥有的,这就包括了,hash 算法定位 hash 桶位置,哈希表由数组和单链表构成,并且当单链表长度超过 8 的时候转化为红黑树,扩容体系,这一切都跟 HashMap 一样。那么除了这么多关键的相同点以外,LinkedHashMapHashMap 更加强大,这体现在:

  • LinkedHashMap 内部维护了一个双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题
  • LinkedHashMap 元素的访问顺序也提供了相关支持,也就是我们常说的 LRU(最近最少使用)原则。

  • LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
  • LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外通过 Entry 维护了一个双向链表。
  • HashMap 元素的遍历顺序不一定与元素的插入顺序相同,而 LinkedHashMap 则通过遍历双向链表来获取元素,所以遍历顺序在一定条件下等于插入顺序。
  • LinkedHashMap 可以通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。

equals 和 hashCode 方法

Equals 是用来比较对象是否相等的,hashCode 主要是用在散列表中便于查找存放的位置。 同一个对象如果没有重写 equals 和 hashCode 方法时,则 equals 相等,hashCode 也相等。 如果重写 equalshashCode,必须定义一致。

Equals 重写时注意:

public boolean equals(Object otherObject){       
//测试两个对象是否是同一个对象,是的话返回true
           if(this == otherObject) {  
               return true;   
           } 
//测试检测的对象是否为空,是就返回false
           if(otherObject == null) {
               return false;       
           }
//测试两个对象所属的类是否相同,否则返回false
           if(getClass() != otherObject.getClass()) {  
               return false; 
           }
//对otherObject进行类型转换以便和类A的对象进行比较
           A other=(A)otherObject; 
           return Object.equals(类A对象的属性A,other的属性A)&&类A对象的属性B==other的属性B……;
    }

HashTable 和 HashMap 的区别

  1. 并发: Hashtable is synchronized, whereas HashMap is not. This makes HashMap better for non-threaded applications, as unsynchronized Objects typically perform better than synchronized ones.
  2. null 值: Hashtable does not allow null keys or values. HashMap allows one null key and any number of null values.

LinkedList 和 ArrayList 的区别

  1. 实现:ArrayList 使用动态数组,LinkedList 使用双向链表
  2. 操作:插入、删除操作,LinkedList 更快
  3. Access:ArrayList 获取元素更快
  4. 内存占用:LinkedList 更多

谈谈 fail fast 机制,ConcurrentModificationException 是怎么抛出来的

fail-fast 机制是 java 集合 (Collection) 中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。 以迭代器迭代过程中修改集合元素为例:

public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};
 
    Iterator iterator = userNames.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}
 

以上代码,使用增强 for 循环遍历元素,并尝试删除其中的 Hollis 字符串元素。运行以上代码,会抛出以下异常:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.hollis.ForEach.main(ForEach.java:22)

具体来说,modCount 是 ArrayList 中的一个成员变量。它表示该集合实际被修改的次数。expectedModCount 是 ArrayList 中的一个内部类——Itr 中的成员变量。创建迭代器时,expectedModCount = modCount 。 那么,接着我们看下 userNames. remove (userName); 方法里面做了什么事情,为什么会导致 expectedModCount 和 modCount 的值不一样。 通过翻阅代码,我们也可以发现,remove方法核心逻辑如下:

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
}

可以看到,它只修改了modCount,并没有对expectedModCount做任何操作。 所以导致产生异常的原因是:removeadd 操作会导致 modCount 和迭代器中的 expectedModCount 不一致。 从而抛出异常。 ArrayList 进行 add,remove,clear 等涉及到修改集合中的元素个数的操作时,modCount 就会发生改变 (modCount++), 所以当另一个线程 (并发修改) 或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使 modCount 发生变化,这样在 checkForComodification 方法中就会抛出 ConcurrentModificationException 异常。 类似的,hashMap 中发生的原理也是一样的。

String、StringBuilder、StringBuffer

String 的不可变性,有什么好处

1. 可以缓存 hash 值 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 2. String Pool 的需要 如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 3. 安全性 String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。 4. 线程安全 String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 Program Creek : Why String is immutable in Java?

Java 怎么实现一个不可变类

  1. 类添加 final 修饰符,保证类不被继承。

  2. 保证所有成员变量必须私有,并且加上 final 修饰

  3. 不提供改变成员变量的方法,包括 setter

  4. 通过构造器初始化所有成员,进行深拷贝 (deep copy)

  5. 在 getter 方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝

介绍一下 public、private、protected、default 访问修饰符

Class. forName 和 ClassLoader 的区别

Class. forName 除了将类的. class 文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static 块。 而classloader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

介绍一下 java 的各种容器,Collection,Map

new 一个对象会发生的事情

对象的创建大概分为以下几步: 1:检查类是否已经被加载; 2:为对象分配内存空间; 3:为分配的内存空间初始化零值(为对象字段设置零值); 4:对对象进行其他设置(设置对象头); 5:执行构造方法。

流程: 1.当虚拟机执行到new 关键字时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程,如果已经被加载,那么进行下一步。 2.当类元信息被加载之后,我们就可以从常量池找到对应的类元信息,通过类元信息来确定类型和后面需要申请的内存大小。 3.对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。(分配完内存后,需要对对象的字段进行零值初始化,对象头除外,零值初始化意思就是对对象的字段赋0值,或者null值,这也就解释了为什么这些字段在不需要进程初始化时候就能直接使用) 4.分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息 5.然后执行对象内部生成的init方法,初始化成员变量值,同时执行搜集到的{}代码块逻辑,最后执行对象构造方法。执行对象的构造方法,这里做的操作才是程序员真正想做的操作,例如初始化其他对象啊等等操作,至此,对象创建成功。

Integer 常量池

String 常量池

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern () 方法在运行过程将字符串添加到 String Pool 中。 当一个字符串调用 intern () 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals () 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

final 修饰变量、方法、类

1. 变量 声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 对于基本类型,final 使数值不变
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的

2. 方法 声明方法不能被子类重写private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 3. 类 声明类不允许被继承

深拷贝和浅拷贝,怎么实现深拷贝

浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象。对原始对象的修改会影响拷贝对象。 深拷贝:拷贝对象和原始对象的引用类型引用不同对象。对原始对象的修改并不会影响拷贝对象。 如何实现:

  1. 手动赋值
  2. 实现 Clonable 接口和 override clone 方法
  3. 序列化与反序列化

介绍一下 Object 的 clone 方法

clone () 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone (),其它类就不能直接去调用该类实例的 clone () 方法。此外,如果要重写 clone 方法则需要类实现 Cloneable 接口。

抽象类和接口的区别

  • 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系(一个类实现了什么接口 = 一个类具备接口定义的功能),它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类
  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

谈谈对 Java 语言的理解