Java基础总结

文章目录

Java 基础

1. 接口和类

Q1: 接口和抽象类区别

抽象类可以提供成员方法的实现细节,接口方法全部是 public abstract 的,jdk8之后接口中允许默认方法实现

抽象类的成员变量可以是各种类型的,接口中的成员变量全部是 public static finnal 的

抽象类可以含有静态代码块和静态方法,接口中不能有静态代码块和静态方法

一个类可以实现多个接口,只能继承一个抽象类

接口是“极度”的抽象

抽象类是一种模板方式模式的实现

Q2: 抽象类可以有构造函数吗

可以有,因为抽象类是要被继承的,而每一个类的默认构造函数,都会隐式的调用super(),编译器将会为抽象类添加默认的无参构造器,如果没有构造器,则子类将无法编译

Q3: Java抽象类可以是final的吗

不可以,因为finnal 修饰的类不可以被继承,abstract 是强制需要继承的,finnal 违反了抽象类的语义

Q4: Java抽象类中可以包含main方法吗

可以,抽象类可以有静态方法的,main 只是一个静态方法

Q4: Java 中如何实现多重继承

使用接口

使用内部类 :内部类继承多个需要的父类,使用了内部类对外部类可见的特性

2. final关键词的使用和区别

Q1:final的含义

final 可以声明类、方法、成员变量和本地变量;一旦将引用声明为final 将不能再改变这个引用,也就是一旦初始化则不可变

Q2: final 使用

final变量经常和static关键字一起使用,作为常量

方法前面加上final 修饰,表示方法不可重写

final修饰类,表示类不可被继承,final类通常功能都是完整的,如String、Integer 等其他包装类

Q3:好处

final关键字提高了性能,JVM 和java应用都会缓存final 变量

final变量可以在多线程环境下安全的共享,而不需要额外的同步开销

final关键字,JVM 会对类,方便和变量进行优化,final常量在定义时初始化使用时不会引起类的初始化,

3.异常分类和处理机制

Q1: java提供两类主要异常:运行时异常和非运行时异常

非运行时异常就是IO异常 和SQL异常,这类异常编译器要求必须手动处理

运行时异常我们可以不处理,当出现异常时,总有虚拟机接管,异常会一层层网上抛,直到遇到catch块,如果没有catch块,则多线程程序由Thread.run抛出,单线程由main函数抛出,如果主线程抛出异常则整个程序退出

Q2: try、catch 和 finally

无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:

  • 在finally语句块中发生了异常
  • 在前面的代码中用了System.exit()退出程序
  • 程序所在的线程死亡
  • 关闭CPU

Q3: finally的执行顺序

finally块一定会执行吗,正常情况下,当在try块或catch块中遇到return语句时,finally语句块在方法返回之前还是之后被执行

public static void main(String[] args) {
        System.out.println(finnalReturn());
    }

    private static String finnalReturn() {
        String s;
        try {
            s = test();
            return s;
        } finally {
            s = "finnal";
            System.out.println("exc finally");
//            return "finally";
        }
    }

    private static String test() {
        System.out.println("exec test");
        return "test";
    }
    
   

输出:
​ exec test
​ exc finally
​ test

执行顺序,try ->return 逻辑(将返回值存储在临时区域中) ->finaly ->return

如果finally 中包含return语句,则会覆盖临时区域中的返回值,return 在finaly之后返回,但无法修改返回的值,只能从新return

4.字符串String

Q1:两种创建字符串区别和原理

使用“引号” 创建字符串,会在常量池创建,并返回常量池对象的引用,使用new 创建字符换会在堆中创建字符串对象,并判断常量池中是否有相等的字符串对象,如果没有则在常量池创建同等的对象(使用同一个char[]),返回堆中对象的引用。

Q2: 判断字符串是否是回文(正反读都一样)

  1. 使用StringBuilder 的 reverse() 翻转对象,然后 equals 判断
  2. 循环遍历,前半段是否和后半段的字符一一对应

Q3: String, StringBuffer,StringBuilder

java语言提供了对字符串连接符(“+”),其他对象转为字符串对象的特别支持。字符串的连接是通过 StringBuilder 或者 StringBuffer 这两个类及它们的append 方法实现的。StringBuffer 和StringBuilder 几乎拥有相同的对外方法,StringBuffer对几乎所有的方法做了synchronized 同步,考虑到线程安全,多线程环境则使用StringBuffer;StringBuilder通过底层数组copy减少了一次,中间字符串对像的分配操作

Q4:intern方法

返回字符串常量池对象,在重复对象上使用该方法可以大量减少内存消耗;当调用此方法时,如果常量池已包含和当前字符相同的对象,则返回常量池字符串引用,否则,这个对象会被加入常量池,返回该常量池对象引用

Q5: String 为什么是不可变的,有什么好处

String 是常量,一旦被创建则值不可改变,String 对象是不可变的共享对象,我想如果是共享对象,那么不使用线程安全策略,那么会有极大的编程风险,其次String对象不可变性指的是值的不可变,String对象的值是存储在常量池中的,也就是堆中,String对象是基于字符数组实现的,当我们改变String对象的值时,不是在原数组上操作,而是新建了一个数组存储新值,原数组并没有改变,这就是String 类的不变性;

由于String是不可变类,所以在多线程中使用是安全的,我们不需要做任何其他同步操作;因为java字符串是不可变的,可以在java运行时节省大量java堆空间。因为不同的字符串变量可以引用池中的相同的字符串。如果字符串是可变得话,任何一个变量的值改变,就会反射到其他变量,那字符串池也就没有任何意义了

Q6: 为什么HashMap的key 总建议用String类型

因为String类中缓存了hash值

5. HashMap

Q1: Hashtable和HashMap的底层实现和区别,HashMap和ConcurrentHashMap的底层实现和区别,Hashmap和Treemap底层实现和区别

HashMap 是基于Hash散列原理实现的,存储结构为:数组+链表(红黑树),当我们使用put 的时候,先取得key 的hash值,然后使用hash &(cap-1) 求得在table数组中的索引位置,如果出现hash碰撞,则以链表的形式存储,如果链表的长度大于等于8,table 数组小于64的时候,执行扩容,大于64的时候则转为红黑树存储。扩容过程中如果红黑树的元素个数小于6个,则转化为链表存储。HashMap初始化默认是容量(table数组长度)是16,默认的扩容因子是0.75,每次扩容比例为2倍,其中容量必须是2的倍数即偶数,这样的话能保证key可以尽可能的均匀分布在数组中。HashMap 的K,V均可以存储NULL值

HashTable 是HashMap 的同步版本,也就是线程安全,内部将一些状态操作全部使用synchronized 同步,不允许K,V 为NULL

ConcurrentHashMap java8 的实现去除了分段锁,而是使用更加细粒度的锁以及CAS,使用synchorized 锁住链表的第一个元素,这样可以支持更高的并发;当一个事务操作发现map正在扩容的时候,会帮助扩容。什么是CAS(compare and swap)呢,每一个CAS操作都包含三个运算参数:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望值的A,则将该地址的值赋为新值B,否则不做任何改动;所以CAS使得同步并不阻塞在编程语言上,二是硬件层面。ConcurrentHashMap 存储K,V 不能为NULL。当我们使用put 的时候,先取的key 的hash值,然后进行hash &(cap-1) 求得在table数组中的索引位置,如果索引位置没有节点,则通过cas 赋值,无需加锁;如果节点为链表且正在扩容,则先帮助扩容;如果没有在扩容,则使用synchronized 锁定链表头一个节点,遍历链表并赋值(如果是红黑树的话,则添加树节点)。ConcurrentHashMap 的size()是实时计算的,因为未加锁,所以这个数量不一定时准确的

TreeMap 是基于红黑树实现的,红黑树是一种平衡二叉树,TreeMap是根据Key 排序的,且K,V 不能为NUll,重要的是左右旋,左旋即父节点变为左子节点,右旋及父节点变为右子节点。

Q2:Hash冲突怎么办,哪些解决散列冲突的方法

开放地址法: 查找下一个直到找到不冲突的值。

再哈希法:哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。

链地址法:也叫拉链发,将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,HashMap 即采用此方法

Q3: HashSet与TreeSet的比较

HashSet 基于HashMap实现,但是value统一为静态的new Object();

TreeSet基于TreeMap 实现,value为一个静态new Object()

6. 队列

Q1: Java中的队列都有哪些,有什么区别

队列:队列是一种特殊的线性容器,它是一种先进先出(FIFO)的数据结构 ,它只允许在容器的头部进行删除操作,而在表的后端进行插入操作。进行插入操作的端成为队尾,进行删除操作的端称为队头

双端队列:双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行

ArrayDeque: 数组双端队列,无界

LinkedBlockingDqueue:基于链表的双端阻塞并发队列,有界

PriorityQueue: 优先级队列,无界

ConcurrentLinkedQueue:基于链表的并发队列,无界

DelayQueue:延期阻塞队列,无界

ArrayBlockingQueue:基于数组的阻塞并发队列,有界

LinkedBlockingQueue:基于链表的阻塞并发队列,有界

PriorityBlockingQueue:带优先级的无界阻塞队列,无界

SynchronousQueue:并发同步阻塞队列,没有存储能力

7. List

Q1: ArrayList是如何实现的,ArrayList和LinkedList的区别?ArrayList如何实现扩容

ArrayList 是基于数组实现的,存储数据的结构是一个Object[],如果未指定大小初始化,则创建一个空数组,通过数组copy扩容,在第一次使用时,扩容至10,随后当容量不能满足时则扩容至原来的1.5倍.

LinkedList 是基于链表实现的,存储数据的结构是一个Node(pre Node,nex Node)节点,每次添加在链表尾部,并且实现了队列操作。

8. 反射与类加载

Q1: 什么是反射机制,作用

反射就是在运行时动态的获取类型的信息(如,接口信息,成员信息,方法信息,构造器),然后根据这些动态获取到的信息创建对象,修改对象,调用方法等。

反射可以用于动态代理

Q2: 如何使用反射

使用Instance.getClass();获取Class对象;使用Class.forName(name)获取Class对象,Class内部有很多方法可以获取该类型的一些基本信息。

常用的方法:

getDeclaredMethods(): 获取该反射类中所有的方法,返回一个String数组类型
getReturnType():获取该方法的返回类型
getParameterTypes():获取某方法的传入类型
getDeclaredMethod("MethodName",class···);获取指定的方法,传入方法名和该方法需要出入的数据类型
getDeclaredConstructors():获取该类所有的构造函数 ,返回一个String数组类型
getDeclaredConstructor(class……):获取特定的构造函数,根据传入的数据类型来进行匹配
getSuperclass():获取该类的父类
getInterfaces():获取该类的实现接口
getDeclaredFields():获取该类的所有属性 返回一个String类数组类型

Q3: 反射的优缺点以及效率

反射的优点是能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性,与 Java 动态编译相结合可以实现无比强大的功能;而其缺点就是性能相对较低,此外使用反射相对来说不安全,破坏了类的封装性。

提高反射效率要考虑的问题如下,首先保证反射 API 最小化,譬如尽量使用 getMethod 直接获取而不是 getMethods 遍历查找获取;其次需要多次动态创建一个类的实例时尽可能的使用缓存,禁止安全检查(setAccessible),可以提高反射的运行速度;

Q4: 都有哪些类加载器,这些类加载器都加载哪些文件,手写一下类加载Demo

编译后的.class 文件是Java 的字节码文件,当需要使用某个类时,JVM 就会加载它的“.class” 文件,并创建对象的Class 对象,将class 文件加载到JVM 内存的过程称为类加载,类加载过程如下:

加载 -> 验证 -> 准备 -> 解析 -> 初始化

加载:查找类字节码,创建Class对象

验证:验证是否符合虚拟机要求,包含,格式验证,元数据验证,字节码验证,符号引用验证

准备:分配内存

解析:将常量池符号引用替换为直接引用

初始化:对超类初始化,静态部分初始化

虚拟机提供了三种类加载器,引导类加载器(bootstrap),扩展类加载器(Extension),系统类加载器(System).

引导类加载器,主要加载JVM自身需要的类,是JVM 的一部分。Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库

系统类加载器,它负责加载系统类路径java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

Q5: 类加载器的双亲委派加载机制

加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它是一种任务委派模式

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

作用,防止类重复加载,防止核心库API 被篡改。

Q6: classforName(“java.lang.String”)和String.class.getClassLoader() .LoadClass(“java.lang.String”) 什么区别啊

Class.forName返回的Class对象可以决定是否初始化。而ClassLoader.loadClass返回的类型绝对不会初始化,最多只会做连接操作

Class.forName可以决定由哪个classLoader来请求这个类型。而ClassLoader.loadClass是用当前的classLoader去请求

8. 内部类

Q1: 内部类与匿名内部类的使用

内部类可以访问外部类引用,使用Outer.this

内部类的创建,(静态内部类)new Outer.Inner(); (非静态内部类) new Outer().new Inner()

匿名内部类使用接口的使用,假如一个局部内部类只被用一次(只用它构建一个对象),就可以不用对其命名了,这种没有名字的类被称为匿名内部类

new SuperType(construction parameters){
    inner class methods and data
};
//or
new InterfaceType(){
    methods and data
};

9. 泛型

Q1: 什么是泛型,泛型有什么好处

泛型是一种参数化类型的机制,是一种编译时类型确认机制。提供了编译期的类型安全,它使得代码可以适用于各种类型,从而提高了代码利用率,可以写出通用的代码

Q2:泛型是如何工作的,什么是类型擦除

泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀

Q3: 如何使用泛型

泛型类

public class BeanTransHelper<S,T> {}

泛型方法

public static <S,T> BeanTransHelper trans(Class<S> s,Class<T> t){
        return new BeanTransHelper<S,T>(s,t);
}

Q4: Array可以用泛型吗

不能,因为泛型对象会在编译阶段擦除泛型类型,这会导致类型检查失效

Q5: list类型对象是否可以赋给一个List类型引用

不能,这里的Object 和 String 仅仅是给编译器做编译的时候检查用的。
这里的List 和List 并没有什么父子类的关系,仅仅是表示一个用来装Obejct型对像,一个用来装String型对像;这种转换只能在子类与父类之间转换,虽然Object是String的父类,但是List和List在编译器看来,是两种完全不同的东西,不允许你这样转换

10. 引用

Q1: 强引用、弱引用和虚引用的概念和使用方式

引用的级别由高到底:强引用 > 软引用 > 弱引用 > 虚引用

强引用是普通的引用,不会被GC回收对象,强引用的对象需要手动置为null 才能被回收

软引用,如果一个对象只有软引用,则除非内存不足,否则不会被GC回收,软引用可以实现内存敏感的高速缓存

弱引用,如果一个对象只有弱引用,则GC运行期间一旦被发现,则直接回收,它比软引用具有更短的生命周期

虚引用,即“形同虚设”,如果一个对象仅持有虚引用,那么它就像跟没有引用是一样的,虚引用只用来跟踪对象被垃圾回收器回收的活动。

11. 同步

Q1: synchronized 和Lock什么区别?sychronized 什么情况情况是对象锁? 什么时候是全局锁,为什么

synchronized 是java 内置的关键字,由JVM 实现,自动获取和释放,可重入但不可中断且非公平,而Lock是一个类,用来实现同步访问,需要手动获取和释放锁,如果不释放锁可能引起死锁;

synchronized 修饰静态方法或者指定(XXX.class)时则为类锁(全局锁),修饰非静态方法及代码片段并使用(this或者某对象)时为对象锁;synchronized 是通过对象监视器(monitor)实现的,Java中每一个对象都可以作为锁,这是synchronized实现的基础,当编译阶段,编译器会在同步块的入口和出口位置分别插入monitorenter 和monitorexit 字节码指令。

sychronized的可重入性是如何实现的:线程已经获取到对象的Monitor后,后续再次获取相同的对象锁时,就不需要再次发起锁请求,因为单线程下是同步执行的,所以可以减少不必要的资源消耗;Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的;每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁

Q2:ThreadLocal 是什么底层如何实现,写一个例子

每一个线程Thread内部持有一个ThreadLocal.ThreadLocalMap 对象,ThreadLocal.get()方法就是获取当前线程,并取得其ThreadLocalMap 对象,并获取之中存储的Entry,将entry.value 转为ThreadLocal 的泛型对象。

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

例子:

//定义ThreadLocal存储
public static final ThreadLocal<TokenInfo> threadLocal = new ThreadLocal<>();

public static void set(TokenInfo tokenInfo) {
	threadLocal.set(tokenInfo);
}

public static TokenInfo get(){
	return threadLocal.get();
}
//防止内存泄露
public static void remove(){
	threadLocal.remove();
}

Entry对象是ThreadLocal的弱引用。所以当线程消亡时,垃圾回收器会将它回收,此时不存在内存泄漏问题;但如果使用线程池,并且线程存在复用的情形,线程不会消亡,这些Entry对象就会发生泄漏,此时就需要我们进行手动清理。

Q3: volitile的工作原理

volitile 会引起CPU 触发缓存一致性协议实现,将当前缓存行数据回写到系统内存,处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期,并强制从系统内存中把数据读取到处理器缓存中。

Q4: CAS知道吗,如何实现的

Compare And Swap : 比较并交换

CAS 有三个操作数,内存值V,旧的预期值A,要修改的新增B,当且仅当预期值A=内存值V时,才将内存值V改为新值B;CAS操作实质上都会调用到 Unsafe 类中的方法,而 Unsafe 中大部分方法是 native 的,也就是说实质使用 JNI 上调用了 C 来沟通底层硬件完成 CAS

12. IO

Q1: BIO,NIO和AIO的区别

进程中IO调用的步骤大概如下:

  1. 进程向操作系统请求数据
  2. 操作系统把外部数据加载到内核缓冲区内(重要)
  3. 操作系统把内核缓冲区数据copy到进程缓冲区 (重要)
  4. 进程获取数据完成自己的功能

IO模型的两个阶段:

  1. 等待I/O数据准备好,这取决于IO目标返回的速度,譬如网速和数据大小。
  2. 数据从内核缓冲区拷贝到进程内

BIO 两个阶段都被阻塞,且等待执行;NIO 第一阶段非阻塞,第二阶段阻塞;AIO 两个阶段都是非阻塞,属于异步方式。

首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO;同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。

由此可见,同步/异步主要指调用端,阻塞/非阻塞主要指服务端。

Q2: Java IO 流中涉及到了哪些设计策略和设计模式

设计策略:链接机制,可以将一个流处理器更另一个流处理器首尾相接,已其中之一的输出作为另一个输入而形成流管道链接。比如:new DataInputStream(new FileInputStream(file));对称性,表现为输入和输出的对称性,如:InputStream 和OutputStream, Reader 和Writer,字节字符对称性,InputStream 和Reader, OutputStream 和Writer.

设计模式:装饰者模式,适配器模式

//把InputStreamReader装饰成BufferedReader来成为具备缓冲能力的Reader。
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

//把FileInputStream文件字节流适配成InputStreamReader字符流来操作文件字符串。
FileInputStream fileInput = new FileInputStream(file); 
InputStreamReader inputStreamReader = new InputStreamReader(fileInput);

Q3: 字节流与字符流有什么区别,如何选择

对于程序运行的物理设备而言,永远只接受字节数据,所以当我们在写数据时,无论使用字节流还是字符流最终都是写的字节流,字符流是字节流的包装类。字节流的操作不会经过内存缓冲区而是直接操作文本本身,而字符流则需要先经过内存缓冲区,然后通过缓冲区再操作文本。

大多数情况下,使用字节流会更好,因为大多数的IO操作都是直接操作磁盘文件的,而这些流在传输时都是以字节的方式进行的。如果操作需要通过IO 在内存中频繁处理字符串的情况,则使用字符流会更好,因为字符流具备缓冲区,提高了性能。

Q4: 什么是缓冲区,有什么作用

缓冲区就是一段特殊的内存区域,很多情况下当程序需要频繁地操作一个资源(如文件或数据库)则性能会很低,所以为了提升性能就可以将一部分数据暂时读写到缓存区,以后直接从此区域中读写数据即可,这样就显著提升了性能。

对于 Java 字符流的操作都是在缓冲区操作的,所以如果我们想在字符流操作中主动将缓冲区刷新到文件则可以使用 flush() 方法操作。

Q5: 讲讲IO里面的常见类,字节流、字符流、接口、实现类、方法阻塞

FileInputStream/FileOutputStream 需要逐个字节处理原始二进制流的时候使用,效率低下

FileReader/FileWriter 需要组个字符处理的时候使用

StringReader/StringWriter 需要处理字符串的时候,可以将字符串保存为字符数组

PrintStream/PrintWriter 用来包装FileOutputStream 对象,方便直接将String字符串写入文件

Scanner 用来包装System.in流,很方便地将输入的String字符串转换成需要的数据类型

InputStreamReader/OutputStreamReader , 字节和字符的转换桥梁,在网络通信或者处理键盘输入的时候用

BufferedReader/BufferedWriter , BufferedInputStream/BufferedOutputStream , 缓冲流用来包装字节流后者字符流,提升IO性能,BufferedReader还可以方便地读取一行,简化编程

13. 其他

Q1: hashcode相等两个对象一定相等吗,equals呢,相反呢

在java中,equals和hashcode是有设计要求的,equals相等,则hashcode一定相等,反之则不然;hashcode相等的两个对象不一定相等,equals相等,则hashcode一定相等,但不一定是同一个对象,相等的对象hashcode 一定相等,equals 也一样相等。

Q2: 字节码都有哪些?Integer x =5,int y =5,比较x =y 都经过哪些步骤

Code:
       0: iconst_5
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: iconst_5
       6: istore_2
       7: aload_1
       8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      11: iload_2
      12: if_icmpne     19
      15: iconst_1
      16: goto          20
      19: iconst_0
      20: istore_3
      21: return

Q3: cgLib知道吗,他和jdk动态代理什么区别,手写一个jdk动态代理

jdk动态代理

首先实现一个InvocationHandler,方法调用会被转发到该类的invoke()方法,然后在需要使用Hello的时候,通过JDK动态代理获取Hello的代理对象

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)

  1. loader,指定代理对象的类加载器;
  2. interfaces,代理对象需要实现的接口,可以同时指定多个接口;
  3. handler,方法调用的实际处理者,代理对象的方法调用都会转发到这里
// 接口
interface Hello{
    String sayHello(String str);
}
// 实现
class HelloImp implements Hello{
    @Override
    public String sayHello(String str) {
        return "HelloImp: " + str;
    }
}
//代理类
class LogInvocationHandler implements InvocationHandler{
    ...
    private Hello hello;
    public LogInvocationHandler(Hello hello) {
        this.hello = hello;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if("sayHello".equals(method.getName())) {
            logger.info("You said: " + Arrays.toString(args));
        }
        return method.invoke(hello, args);
    }
}
// 2. 然后在需要使用Hello的时候,通过JDK动态代理获取Hello的代理对象。
Hello hello = (Hello)Proxy.newProxyInstance(
    getClass().getClassLoader(), // 1. 类加载器
    new Class<?>[] {Hello.class}, // 2. 代理需要实现的接口,可以有多个
    new LogInvocationHandler(new HelloImp()));// 3. 方法调用的实际处理者
System.out.println(hello.sayHello("I love you!"));

CGLIB动态代理

首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法,然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象

//被代理类
public class HelloConcrete {
    public String sayHello(String str) {
        return "HelloConcrete: " + str;
    }
}

/ CGLIB动态代理
// 1. 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
class MyMethodInterceptor implements MethodInterceptor{
  ...
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        logger.info("You said: " + Arrays.toString(args));
        return proxy.invokeSuper(obj, args);
    }
}
// 2. 然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));

cglib 代理的是普通的类,而jdk代理的是接口。jdk 只能代理接口,而cglib只能代理普通类(所以类不能定义为final)

Q4: 序列化和反序列化

Java 对象的生命周期是依托于JVM 的,如果需要在JVM 之外能够保存指定对象,并在任一时刻JVM 可以读取被保存的对象,Java对象序列化与反序列化就是实现该功能。

在Java中,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化

通过ObjectOutputStream.writeObject()ObjectInputStream.readObject()对对象进行序列化及反序列化

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID

序列化并不保存静态变量

transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值