我的成长磨练,每天写博客,年轻人,卷起袖子,来把手弄脏吧! ------ 博客首页

基础知识-Java基础

Java基础问题集

0. 并发包知识点

TODO

1. HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。

image

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;
如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;
对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。
所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap创建的时候并未完成对主体数组的创建,直到第一次put操作时才将entry数组初始化,初始化的默认大小为16,当然也可以在创建HashMap的时候指定默认大小,避免之后的扩容。

  • 为什么HashMap的每次扩容都是2的N次幂大小的数组呢? 和resize方法有关,在扩容HashMap的时候,需要重新计算索引位置,2次幂的扩容可以保证之后的索引分布仍然均匀。
  • 老调重弹一个问题,为什么在修改equals方法时最好一并修改hashCode方法? 因为在HashMap这种常用的数据结构中,如果不重写hashCode方法可能造成,
    key虽然相同(根据equals方法),但是在hashMap中获取对应值时却取不到应取到的值
  • HashMap为什么线程不安全 首先在并发执行PUT操作时,如果同时出现hash冲突,则可能导致数据被覆盖。其次在JDK1.7的时候,扩容后转移数据的transfer方法,
    多线程并发扩容,因为采取的是头插法,可能导致链表出现环形链或者数据丢失的情况,这个问题在1.8中得到了修复

源码中其他几个重要字段

源码中其他几个重要字段(点击查看代码)
// 实际存储的key-value键值对的个数
transient int size;
// 阈值,当table == {}时,该值为初始容量(初始容量默认为16);
// 当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。
// HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;
// 负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
// 用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,
// 如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount

2、HashMap,HashTable,ConcurrentHashMap的区别。

  • HashMap线程不安全(hash冲突并put时 & 扩容resize时有安全问题),数据结构为 数组+链表,允许null为Key,效率高于HashTable;
  • HashTable线程安全(因为方法均使用synchronized修饰),数据结构为 数组+链表,不允许null为Key,效率低于HashMap;
  • ConcurrentHashMap线程安全(锁的粒度更小,使用segment单元作为锁单元,避免扩容时的不安全),数据结构为segment数组+链表,允许null为key。
    单元Segment本身就是一个ReentrantLock,理论上每个Segment单独访问时才有竞争问题,而且Entry的value是由volatile修饰的,内存修改可见

3、java中四种修饰符的限制范围。

  • public 公开访问
  • protected 继承的类可以访问以及和private一样的权限
  • default 包内允许访问
  • private 类内私有访问

4、Object类中的方法。

  • toString(): 将对象转换为String进行输出,默认使用ClassName + hashCode方法的结果组合而成
  • equals(): 判断对象是否相同
  • hashCode(): 用于对象的hashCode散列算法,比如HashMap就是利用hashCode确定对象是否应该落入同一个数组位或者Segment

5、接口和抽象类的区别,注意JDK8的接口可以有实现。

  • 1.接口能够多实现,抽象类只能单继承
  • 2.抽象类的方法可以被abstract,public,protected修饰
  • 3.抽象类既可以定义子类需要实现的抽象方法也可以定义公用的具体方法。接口不能带有方法体(1.8更新后可以带有默认方法体)
  • 4.抽象类中可以含有静态代码块和静态方法,但是接口不能含有
  • 5.抽象类可以含有构造方法,接口不能含有构造方法
  • 6.在设计层面上,接口是对对象特征的抽象,其中并不含有默认的方法定义(1.8后有所变化)。抽象类则更像是模板,用于对特征的描述和集体方法的抽取

6、动态代理的两种方式,以及区别。

  • 1.JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
  • 2.CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

区别是:JDK只能对实现接口的类生成代理;CGlib是针对类实现代理,并指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类

另:SpringBoot2之前默认使用JDK动态代理,之后为了兼容注入实现类的方法,采用了cglib作为默认动态代理 在jdk1.8中JDK动态代理的效率已经提高了,可以根据实际情况(是否频繁创建决定)

  • 1.JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm,通过修改字节码生成子类。
    所以jdk动态代理的方式创建代理对象效率较高,执行效率较低,cglib创建效率较低,执行效率高
  • 2.JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法,
    CGLIB则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。
点击查看代码
public class Demo{
    
    public static void main(String[] args) {
        UserManager userManager = (UserManager)new CGLibProxy()
            .createProxyObject(new UserManagerImpl());
        System.out.println("CGLibProxy:");
        userManager.addUser("tom", "root");
        System.out.println("JDKProxy:");
        JDKProxy jdkProxy = new JDKProxy();
        UserManager userManagerJDK = (UserManager)jdkProxy.newProxy(new UserManagerImpl());
        userManagerJDK.addUser("tom", "root");
    }
    
    /**
     * CGlib动态代理类
     */
     public class CGLibProxy implements MethodInterceptor {    
        
        // CGlib需要代理的目标对象
        private Object targetObject;
        
        public Object createProxyObject(Object obj) {
            this.targetObject = obj;
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(obj.getClass());
            enhancer.setCallback(this);
            Object proxyObj = enhancer.create();
            return proxyObj;
        }
        
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            Object obj = null;
            // 过滤方法
            if ("addUser".equals(method.getName())) {
                // 检查权限
                check();
            }
            obj = method.invoke(targetObject, args);
            return obj;
        }    
        
        private void check() {
            System.out.println("检查权限:check()!");
        }
    }

    
    /**
     * JDK动态代理类
     */
    public class JDKProxy implements InvocationHandler {    
        
        // 需要代理的目标对象
        private Object targetObject;    
        
        public Object newProxy(Object targetObject) {
            // 将目标对象传入进行代理    
            this.targetObject = targetObject;
            // 返回代理对象 
            return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
        }    
        
        // invoke方法
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 进行逻辑处理的函数
            check();
            Object ret = null;
            // 调用invoke方法
            ret = method.invoke(targetObject, args);
            return ret;
        }    
        
        private void check() {
            // 模拟检查权限   
            System.out.println("检查权限:check()!");    
        }    
    } 

    /**
     * 目标接口类
     */
    public interface UserManager {    
        public void addUser(String id, String password);    
        public void delUser(String id);    
    }


    /**
     * 接口实现类
     */
    public class UserManagerImpl implements UserManager {    
        
        @Override
        public void addUser(String id, String password) {    
            System.out.println("调用了UserManagerImpl.addUser()方法!");
        }    
        
        @Override
        public void delUser(String id) {    
            System.out.println("调用了UserManagerImpl.delUser()方法!");
        }    
    }

}

参考资料:

posted on 2022-09-06 19:21  SethMessenger  阅读(32)  评论(0)    收藏  举报

导航