hashCode() 与 equals()
hashCode() 的作用是获取哈希码(int 整数)
这个哈希码的作用是确定该对象在哈希表中的索引位置。
- 为什么要有 hashCode?
用于比较两个对象是否相等时,先比较hashCode是否相等,若相等则调用 equals() 方法来检查 hashCode 相等的对象是否真的相同,减少了 equals 的次数,相应就大大提高了执行速度。 - 为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。
所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
自动装箱与拆箱了解
Integer i = 10; //装箱
等价于 Integer i = Integer.valueOf(10)
int n = i; //拆箱
等价于 int n = i.intValue();
成员变量与局部变量
1、成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;
2、成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;
3、成员变量和局部变量都能被 final 所修饰。
4、如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
5、成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
6、成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
反射
-获取 Class 对象
Class alunbarClass = TargetObject.class;
Class alunbarClass1 = Class.forName(“cn.TargetObject”);
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
Class clazz = ClassLoader.loadClass(“cn.TargetObject”);
-
基本操作
/**
* 获取TargetObject类的Class对象并且创建TargetObject类实例
/
Class<?> tagetClass = Class.forName(“cn.javaguide.TargetObject”);
TargetObject targetObject = (TargetObject) tagetClass.newInstance();
/*
* 获取所有类中所有定义的方法
/
Method[] methods = tagetClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
/*
* 获取指定方法并调用
*/
Method publicMethod = tagetClass.getDeclaredMethod(“publicMethod”,
String.class);publicMethod.invoke(targetObject, "JavaGuide"); /** * 获取指定参数并对参数进行修改 */ Field field = tagetClass.getDeclaredField("value"); //为了对类中的参数进行修改我们取消安全检查 field.setAccessible(true); field.set(targetObject, "JavaGuide"); /** * 调用 private 方法 */ Method privateMethod = tagetClass.getDeclaredMethod("privateMethod"); //为了调用private方法我们取消安全检查 privateMethod.setAccessible(true); privateMethod.invoke(targetObject);
}
代理模式
我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作
- 静态代理
实现步骤:
定义一个接口及其实现类;
创建一个代理类同样实现这个接口 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
- 动态代理
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。 - JDK 动态代理机制
在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。
使用步骤:
定义一个接口及其实现类; 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
public static Object newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h)throws IllegalArgumentException{
......
}
public interface InvocationHandler {
/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;}
- CGLIB 动态代理机制
在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。
使用步骤:
1、定义一个类;
2、自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
public class DebugMethodInterceptor implements MethodInterceptor {
/**
* @param o 代理对象(增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}
}
3、通过 Enhancer 类的 create()创建代理类;
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
IO模型
泛型
Java 中 3 种常见 IO 模型
- BIO (Blocking I/O)
- NIO (Non-blocking/New I/O)
同步非阻塞 IO 模型。
应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的
I/O 多路复用模型
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。 - AIO (Asynchronous I/O)
集合
-
HashMap 查询 删除的时间复杂度
分情况O(1),O(n),O(logn) -
HashMap底层实现
JDK1.8 之前 HashMap 底层是 数组和链表,HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 -
ConcurrentHashMap 和 Hashtable 的区别
1、底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构数组+链表/红黑二叉树。
Hashtable 数组+链表 的形式
2、 实现线程安全的方式(重要):
① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get
-
ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N))) synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
-
HashSet,LinkedHashSet和TreeSet之间的异同?
HashSet中不能有相同的元素,可以有一个Null元素,存入的元素是无序的。
HashSet如何保证唯一性?
1).HashSet底层数据结构是哈希表,哈希表就是存储唯一系列的表,而哈希值是由对象的hashCode()方法生成。
2).确保唯一性的两个方法:hashCode()和equals()方法。
添加、删除操作时间复杂度都是O(1)。
非线程安全
LinkedHashSet中不能有相同元素,可以有一个Null元素,元素严格按照放入的顺序排列。
LinkedHashSet如何保证有序和唯一性?
1).底层数据结构由哈希表和链表组成。
2).链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。
添加、删除操作时间复杂度都是O(1)。
非线程安全
TreeSet是中不能有相同元素,不可以有Null元素,根据元素的自然顺序进行排序。
TreeSet如何保证元素的排序和唯一性?
底层的数据结构是红黑树(一种自平衡二叉查找树)
添加、删除操作时间复杂度都是O(log(n))
非线程安全 -
Heap是什么?和Stack的区别
栈(stack)与堆(heap)都是Java用来在RAM中存放数据的地方。
堆(heap)
- Java的堆是一个运行时数据区,类的对象从堆中分配空间。堆内存用来存放由new创建的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。(堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 )
- 堆的优势是可以动态地分配内存空间,需要多少内存空间不必事先告诉编译器,因为它是在运行时动态分配的。但缺点是,由于需要在运行时动态分配内存,所以存取速度较慢。
- 堆的物理地址分配对对象是不连续的。因此性能慢些。
- 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈(stack)
- 栈中主要存放一些基本数据类型的变量(byte,short,int,long,float,double,boolean,char)和对象的引用。(栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行)
- 栈的优势是,存取速度比堆快,栈数据可以共享。但缺点是,存放在栈中的数据占用多少内存空间需要在编译时确定下来,缺乏灵活性。
- 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
- 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。