与java 相关:pdai.tech/md/java/bas…
静态方法和普通方法的区别:
-
加载上
-
静态方法 在方法区加载,普通方法在jvm栈中加载
-
使用上
-
1、静态方法在类加载后就能使用类名调用方法,类名+方法名,而非静态方法只有在实例化对象中才能调用,对象名+方法名
-
2、静态方法只能用调用静态变量,而非静态方法既能调用静态变量又能调用静态方法
-
3、静态方法的生命周期同类的生命周期,占用内存 但是速度快。而非静态方法同对象的生命周期,可以被垃圾回收,但是比较慢。
-
4、所以一般频繁使用的方法被定义为静态的方法,不常用的方法被定义成普通方法
-
内部类的实例化对象需要绑定一个外围类的实例化对象,而静态嵌套类的实例化对象不能也无法绑定外围类的实例化对象。
private、static、final方法或者构造器可以被重写吗?【所以不能使用cglib代理】
-
静态绑定(Early Binding)
- ✅
private、static、final方法和构造器 的调用版本在编译期即可确定,属于静态绑定。 - ✅ 这些方法在类加载阶段会将符号引用转为直接引用,无需运行时动态解析。
- ✅ 构造器 确实不能被继承或重写,子类只能通过
super()调用父类构造器。invokestatic(静态方法)和invokespecial(私有方法/构造器) :
- ✅
-
动态绑定(Late Binding)与多态
- ✅ 普通实例方法(非
private/static/final)通过动态绑定实现多态,JVM 使用方法表(vtable)在运行时确定具体调用的方法。invokevirtual(虚方法),final也用的虚方法,invokevirtual触发虚方法表的查找
- ✅ 普通实例方法(非
final 关键字底层原理
变量:编译期内联 + 运行时禁止重新赋值。
方法:编译器禁止重写 + JIT 内联优化(类解析)。
类:禁止继承 + 隐含方法不可重写。
线程安全:final 字段的初始化安全由 JMM 保证。
-
编译器会在final域写后插入一个storestore屏障,在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排
-
先读对象的引用,后读final变量,编译器在读final域操作的前面,插入一个loadload屏障
-
初次读一个包含final域的对象引用,与随后初次读这个final域这两个操作之间不能重排序
This和super
-
This关键字使用在一个成员函数的内部
-
当局部变量和成员变量重名的时候,在方法中使用this表示成员变量以示区分
-
This用法表示当前对象的引用
-
在一个构造器中调用构造器需要使用this
-
Super:
-
Java 里在类中用super调用父类构造函数时,必须是子类的第一条语句。
-
子类中的成员变量或方法与父类中的成员变量或方法同名,在方法中使用super表示父类方法或者成员
-
super(参数):调用父类中的某一个构造函数
-
Super super 相当于是指向当前对象的父类
快慢速度对比
性能排序(从最快到最慢)
-
final方法(jit 完全内敛,当成一个方法使用了)- 最快(静态绑定 + 内联优化)
-
super父类方法(invokespecial 直接跳转目标方法调用)- 次快(静态绑定,但无内联优势)
-
this普通方法- 中等(虚方法表动态分派)
-
GeneratedMethodAccessor(invokevirtual 触发虚方法表的查询)- 最慢(反射优化后的代理调用,仍有开销)
Java 代码是怎么运行的:
-
A[编写Java源代码] --> B[编译为字节码.class字节码文件【词法分析 → 语法分析 → 语义分析 → 生成字节码】]----> C[类加载]----> D[字节码验证]-> E[解释执行/JIT编译] -> F[优化机器码执行]
-
Java 源文件名就是该源文件的中public类的名称,一个java源文件可以包含多个类,但只允许一个类为public,在jdk目录下,有两个.exe文件,即javac.exe(编译源代码,xxx.java文件) 和 java.exe(执行字节码,xxx.class文件).
-
javac.exe编译java源代码时,java源代码有几个类,就会编译成一个对应的字节码文件(.class文件)其中,字节码文件的文件名就是每个类的类名。需要注意的是,类即使不在源文件中定义,但被源文件引用,编译后,也会编程相应的字节码文件。使用javac编译器将源代码编译为字节码
-
执行java源文件,用java.exe执行即可。
-
JVM JDK JRE
-
JVM:JVM即为Java虚拟机,它是Java跨平台实现的最核心的部分。
-
所有的Java程序首先被编译成java.class字节码文件这种文件可以在JVM上执行,JVM在执行字节码文件时,把其翻译成具体平台上的机器指令执行。
-
JRE:是Java Runtime Environment缩写,指Java运行环境。它包含Java虚拟机(jvm)、Java核心类库和支持文件
-
JDK是(Java Development Kit)的缩写,指的是Java开发工具包。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具
-
类加载时机
-
1 创建一个实例,new一个对象的时候
-
2 访问某个类或接口的静态变量,或者对该静态变量赋值
-
3 调用类的静态方法
-
4 反射初始化一个类的子类(会首先初始化子类的父类)
-
6 反射
-
5 Jvm 启动时标明启动类,即文件名和类名相同的类
- 类加载过程
-
一个java文件从被加载到被卸载这个生命过程,总共要经历5个阶段,JVM将类加载过程分为:
-
加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载
-
加载:指的是把从各个来源的class字节码文件,通过类加载器加载到内存中
-
验证:保存加载进来的字节流符合虚拟机规范,不会造成安全性错误
-
准备:主要为static修饰的变量分配内存,并且为(static+final+基本类型和字符串常量)赋予初始值
-
解析:将常量池内的符号引用替换为直接引用。符号引用一种逻辑描述,包含类/方法/字段的名称和描述符。直接引用,具体的内存地址或偏移量,直接指向目标。
-
初始化:为类的静态变量赋予初始值 static修饰的变量。以及(static+final+引用类型)
-
类加载器
-
Bootstrap ClassLoader JDK\jre\lib
-
Extension ClassLoader JDK\jre\lib\ext
-
Application ClassLoader ClassPath
-
自定义类加载器 自定义路径
-
双亲委派加载模式(伪代码)
- protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- Class<?> c = findLoadedClass(name);// 首先,检测该类是否已经加载
- if (c == null) {
-
- try {
- if (parent != null) {
- //父加载器不为空则调用父加载器的loadClass
- c = parent.loadClass(name, false);
- } else {
- //父加载器为空则调用Bootstrap Classloader
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- if (c == null) {
- //如果仍然无法加载成功,则调用自身的findclass进行加载
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);//可以不完全地(不带解析的)装入类,也可以完全地(带解析的)装入类。
- }
- return c;
- }
}
- 好处
- 1 避免重复加载 ,当父类已经加载了该类的时候,就没必要子类加载器再加载一次。
- 2沙箱安全:防止恶意代码污染java源代码。比如我定义了一个类名为String所在包为java.lang.因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染
- 打破双亲委派(类加载器里面要加载类)
- 双亲委派模型“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。(OSGI模块热部署,方便模块管理)
- 沿用双亲委派机制自定义类加载器很简单,只需继承ClassLoader类并重写findClass方法即可(只需写自己怎么加载的)
- 打破双亲委派机制则不仅要继承ClassLoader类,还要重写findClass方法还要重写loadClass方法(不光要写自己怎么加载,还要写以什么方式进行加载(打破双亲委派))
- JVM 类加载约束
- 已加载类的结构不可变(方法表、字段布局、继承关系等固定);
- 1、方法调用静态绑定:编译时确定的方法签名必须与运行时一致
- 2、字段访问偏移量固定:对象内存布局在类加载时确定
- 3、JIT 编译假设:热点代码编译依赖稳定的类结构
- 可修改的对象,方法体实现 ,注解信息,静态字段值
在编译阶段生成方法 用APT
ASM 是一个 Java 字节码操作和分析框架,它允许开发者直接读取、修改和生成 Java 类文件的字节码。
java agent 和ASM一起使用。需要修改已加载的类 → Java Agent + ASM
仅需生成新类 → 单独使用 ASM (如编译期生成代理类)
Java agent(编译后,加载,运行时)
Instrumentation API:java.lang.instrument 包提供的核心接口 提供与 JVM 交互的入口,控制类加载流程,但不直接操作字节码
Premain 做基础增强,Agentmain 按需动态调整
静态加载(Premain)JVM 启动时加载:在目标应用的主类(main 方法)执行前加载 Agent。
动态加载(Agentmain)运行时动态附加:向已运行的 JVM 进程注入 Agent。
public interface ClassFileTransformer { byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer); }
void addTransformer(ClassFileTransformer transformer);注册Transformer
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; 强制对已加载的类进行重新转换。JVM从初始类定义重新加载字节码;调用所有已注册的ClassFileTransformer.transform()应用修改后的字节码,只能修改方法体,不能增减字段/方法;
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException;它允许直接替换 JVM 中已加载类的字节码
boolean isModifiableClass(Class<?> theClass);检查是否可以被重新定义,重新转换
怎么向一个类里新增方法呢
new对象的过程
- Mark word:Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。
- Class pointer:对象指向它的类的元数据指针,虚拟机通过这个指针来确定这个对象是哪个类
- 实例数据:对象真正存储的有效信息,对象中字段对应的具体值。相同宽度的字段是分配到一起的
- 对齐填充:虚拟机要求对象的起始地址必须是8字节的整数倍,当实例数据没有对齐的时候,就需要对齐填充补全,对象头刚好是8字节的倍数。
- Mark word
-
创建对象的4种方法:
-
1 使用new关键字
-
2 通过反射
-
3 使用clone()方法
-
4使用序列化方法
-
Object 下都有什么方法:
-
Hashcode
-
Clone
-
Series
-
Class
-
Wait
-
Nofy
-
Student dongshuzhan = new Student(123):
-
(1)先看是否进行类的加载,如果没有进行类的加载,先进行类的加载
-
(2)然后进行内存分配,根据使用的垃圾收集器的不同使用不同的分配机制。
-
[与进程空间划分的区别]
-
2.1. 指针碰撞(Bump the Pointer):假设Java堆的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
-
2.2. 空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用。
-
内存分配完后,虚拟机需要将分配到的内存空间中的数据类型都初始化为零值(不包括对象头)
-
(3)虚拟机要对对象头进行必要的设置 ,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。
-
(4)调用对象的init()方法 ,根据传入的属性值给对象属性赋值【例如 123 复制】
-
(5)然后使对象的引用指向分配的内存地址【dongshuzhan指向这个实例对象】
hashcode 和 equals
-
如果要添加一个类的新对象,不但要重写hashcode,equal。还要重写comparator接口
-
这两种比较器Comparable和Comparator,后者相比前者有如下优点:
-
个性化比较:如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法。
-
解耦:实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。从这个角度说,其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。
-
为什么重写equals要重写hashcode:
-
Hashcode 底层实现 是c++代码实现的:
-
- 随机数
-
- 基于内存地址生成
-
- 固定值:1,用来测试
-
- 自增
-
- 利用位移生成随机数
-
不重写hashcode,此时的hashcode放在对象头里,重写hashcode则不放
-
如果没有从写hashcode,equals方法,那么他就会调用Object的equals,hashcode。这两个方法对比的都是对象的内存地址。所以不写会在hashmap中出错。
-
从写equal对比两个对象是否相等,而不是内存地址,根据对象属性的值来计算hashcode不是地址值
-
==和equals 的区别
-
总结的来说:
-
1)对于==,比较的是值是否相等
-
如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
-
如果作用于引用类型的变量,则比较的是所指向的对象的地址
-
对于equals方法,
-
注意:equals方法不能作用于基本数据类型的变量。
-
equals继承Object类,比较的是是否是同一个对象
-
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
-
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
java栈
在 Java 中,栈内存(Stack Memory) 是线程私有的运行时数据区,主要用于存储方法调用时的临时数据。每个线程在创建时都会分配一个独立的栈。
栈帧(Stack Frame) 每个方法调用时,JVM 会在栈中压入一个 栈帧,方法结束时弹出。栈帧包含以下内容:
局部变量表(Local Variable Array)
存储方法的 参数 和 局部变量(包括基本类型和对象引用)。
操作数栈(Operand Stack),指向 运行时常量池 中该方法的符号引用,用于支持多态(如虚方法调用)。
动态链接(Dynamic Linking)
方法返回地址(Return Address)
红黑树(5个性质):
-
性质1 根节点是黑色。
-
性质2 所有叶子都是黑色。(叶子是NUIL节点)
-
性质3 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
-
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-
他是一种放开的平衡树,最深的树是最短的树二倍,最短的树全是黑色结点,最深的树红黑交替的。(这样才能满足性质)
-
时间复杂度: o(lgn)
HASHMAP
-
默认容量 1<<4=16,最大容量1<<30
-
负载因子,默认值=0.75
-
treefy-Threshold=8
-
Min-Treeify-capacity=64
-
Untreeify_Threshold=6
-
hash函数获取hashcode:(h=key.hashcode)^(h>>16)
-
h与h的高16位异或,扰动hashcode减少碰撞
-
当数组长度小,hashcode直接按位与,实际上只使用了哈希值得后四位,如果哈希值高度变化大,低位变化小,这样很容易造成哈希冲突,所以这里把高位和低位都利用起来。
-
散列函数实际是取模,hash%length,但是计算机直接求余效率不如位运算高,所以使用hash&(length-1)
-
如果想hash%length=hash&(length-1),length必须是2的n次幂,所以在构建hashmap时,初始容量实际为大于传入的容量的最小的2次幂
-
为什么JDK1.8中链表转为红黑树的阈值是8
-
1 、6和8中间有个7可以有效防止链表和树频换的转换,假设都是一个数8,插入和删除在8徘徊就会频换的转化
-
2 、红黑树中的TreeNode是链表中的Node所占空间的2倍,虽然红黑树的查找效率为o(logN),要优于链表的o(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。固,要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。
-
3、之所以是8,是因为Java的源码贡献者在进行大量实验发现,容器中节点分布在hash桶中的频率遵循泊松分布,hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此次操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,这个时候,就应该将链表转换为红黑树了,也就是为什么链表转红黑树的阈值是8。
-
Resize
-
1、当hashmap元素总数超过数组大小*负载因子
-
2、Hashmap中其中一个链表的对象个数如果达到8,数组长度没有达到64时。
-
在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引就没改变,是1 的话索引就变为(原位置+旧容量),每一次扩容都是原容量x2大小,hash值是32位。
-
HashMap与HashTable区别
-
因为 Hashmap是非线程安全的,HashTable是线程安全点的,加上synchronic锁,涉及cpu从用户态到和心态的切换,故hashmap效率高于hashTable,最好用ConCurrentHashMap
-
所以 Hashtable中,key和value都不允许出现null值 而HashMap可以
那你谈谈快速失败(fail-fast)和安全失败(fail-safe)的区别
-
如果采用快速失败机制,那么在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行增加、删除、修改,或者 A 线程在遍历过程中对集合进行增加、删除、修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。
-
原因是迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。
-
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
-
java.util 包下的集合类都是快速失败的。
-
那么在遍历时不是直接在集合内容上访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。
-
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常。
-
java.util.concurrent 包下的并发容器都是安全失败的
Integer与int的区别
-
1、int 和Integer 进行比较,Integer会进行拆箱,转为int值与int进行比较。
-
2、对Integer 直接赋予int类型值,在-128<=x<=127的整数直接缓存在Integercache中,不会创建新的Integer对象,当大于这个范围的时候就会进行装箱创建Integer对象。
-
Show case:
-
(一)
-
Interger i= new Integer(0);
-
i++;
-
此时的i已经不是原来的i对象了。i++; i=i+1;
-
I=i+1;会发生拆箱和装箱操作。装箱的时候会new 一个interger;
-
(二)
- Java异常:
-
error与exception区别:
-
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。如虚拟机错误,内存溢出,线程死锁。
-
Exception(异常):是程序本身可以处理的异常。可以分为运行时错误,包括空指针,数组下标越界,类型转换异常。算数异常非运行时异常:IO异常,SQL异常
-
throw出现在函数体,throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
-
两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理
-
重点:Return 是程序的结束
- class test{
- public static int test1(){
- try{
- int i=5;
- int val=0;
- System.out.println(111);
- return 11;
- } catch (Exception e) {
- System.out.println(112);
- return 12;
- }finally {
- System.out.println(113);
- return 13;
- }
- }
- public static void main(String[] args) {
- System.out.println(test1());
- }
- }
- 答案
- 111
- 113
- return 13
-
- class test{
- public static int test1(){
- try{
- int i=5;
- int val=0;
- i=i/val;
- System.out.println(111);
- return 11;
- } catch (Exception e) {
- System.out.println(112);
- return 12;
- }finally {
- System.out.println(113);
- return 13;
- }
- }
- public static void main(String[] args) {
- System.out.println(test1());
- }
- }
- 答案
- 112
- 113
- return 13
- class test{
- public static int test1(){
- try{
- int i=5;
- int val=0;
- System.out.println(111);
- return 11;
- } catch (Exception e) {
- System.out.println(112);
- return 12;
- }finally {
- System.out.println(113);
- 一个函数return后就会被销毁
- }
- }
- public static void main(String[] args) {
- System .out.println(test1());
- }
- }
- 111
- 113
- 11
-
-
Finally:
-
1、 finally与try和catch一起用于异常处理,finally块一定会被执行,无论在try块中是否发生异常
-
finalize:是一个方法,当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。他的作用就是将对象从内存中清理出去之前做必要的清理工作。
-
Java 八大数据类型:
-
Byte short int long double float char boolean
-
(1字节2字节4字节8字节8字节4字节2字节1字节)
-
这些类型有底到高(byte,short,char)--int--long--float--double
-
低级到高级的自动类型转换,高级到低级的强制类型转换,包装类过渡类型能够转换。
-
其他数据类型-------->字符串(基本数据类型----->包装类型----->toString()到字符串)
-
字符串---------->数字类型(1、一种是将其转换成对应的ASCII码 2、字节转为字符{可以用Chracter.getNumericValue(char ch)})
序列化和反序列化:(很少用java原生的序列化了)
-
序列化:把对象转换为字节序列的过程称为对象的序列化.
-
序列化步骤:
-
步骤一:创建一个ObjectOutputStream输出流;
-
步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象
-
反序列化:把字节序列恢复为对象的过程称为对象的反序列化.
-
反序列化步骤:
-
步骤一:创建一个ObjectInputStream输入流;
-
步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象;
-
什么进行时候序列化和反序列化:
-
把内存中的对象进行持久化或者网络传输的时候。
-
网络传输:JSON格式实际上就是将一个对象转化为字符串, 所以服务器与浏览器交互时的数据格式其实是字符串。而String里实现了序列化接口。
-
持久化:把对象持久化到数据库中,实际上不是把对象持久化,而是把对象的属性持久化,对象的属性都实现了序列化接口。
-
如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
-
怎么序列化:
-
实现Serializable接口,实现该接口后,JvM会在底层帮我们序列和反序列化
-
为什么要在类中指定serialVersionUID:
-
不显示指定serialVersionUID,jvm会根据类属性自动生成一个序列化ID。当反序列化的时候需要比对UID,如果类的属性发生了变化,对应的UID 就会发生改变,反序列化失败。
-
所以需要显示的指定serialVersionUID
-
不会序列化的情况:
-
transient关键字修饰的属性不会被序列化, static属性也不会被序列化
Java 反射:
-
反射是在运行状态中,判断任意一个对象所属的类,得到任意一个类所具有的成员变量和方法
-
能够调用对象的方法和属性,能够构造任意一个类的对象。这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制
Java反射的优点:
-
反射提高了程序的灵活性和扩展性,降低耦合性,提高自适应能力。
-
它允许程序创建和控制任何类的对象,无需提前硬编码目标类
Java效率低的原因:
-
Method
-
Invoke
-
方法会对参数做封装和解封操作
-
需要检查方法的可见性
-
需要校验参数
-
反射方法难以内联
-
JIT无法优化
反射原理
DelegatingClassLoader
-
出于性能的考虑,JVM会在反射代码执行一定次数后,通过动态生成一些类来将”反射调用”变为“非反射调用”,以达到性能更好。而这些动态生成的类的实例是通过软引用SoftReference来引用的。【原理】
-
我们知道,一个对象只有软引用SoftReference,如果内存空间不足,就会回收这些对象的内存。进而导致需要频繁重新生成这些动态类。
-
(一)java 获取反射常使用的三种方式,获取class对象的方式
-
(一个类在内存中只有一个class对象,一个类被加载后,类的整个结构都会被封装在class对象中):
-
1、通过new对象实现反射机制( 对象.getClass() )
-
对类进行静态初始化、非静态初始化返回运行时真正所指的对象
-
2、通过路径实现反射机制( Class.forName("包名.类名") )
-
装入类,并做类的静态初始化,返回Class的对象
-
3、 通过类名实现反射机制 ( 类名.Class )
-
类A装入内存(前提是:类A还没有装入内存),不对类A做类的初始化工作,返回Class的对象
-
反射:
-
Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
-
#Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。
-
classloader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。主要是loadclass是保护类型的方法,最后一个参数默认false
-
(二)通过反射创建类对象
-
1、通过 Class 对象的 newInstance() 方法
-
Class clz = Apple.class;
-
Apple apple = (Apple)clz.newInstance();
-
2、 通过 Constructor 对象的 newInstance() 方法
-
Class clz = Apple.class;
-
Constructor constructor = clz.getConstructor(String.class, int.class);
-
Apple apple = (Apple)constructor.newInstance("红富士", 15);
-
区别:通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法
-
(三)通过反射获取类属性、方法、构造器
-
通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性
-
Class clz = Apple.class; Field[] fields = clz.getFields();
-
for (Field field : fields)
-
{ System.out.println(field.getName()); }
-
使用 Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性
-
Class clz = Apple.class;
-
Field[] fields = clz.getDeclaredFields();
-
for (Field field : fields) { System.out.println(field.getName()); }
-
//获取的方法
-
getDeclaredMethod(方法名,参数类型类集合) 得到获取的是类自身声明的所有方法,包含public、protected和private方法
-
getMethod () 获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。
Java是值传递还是引用传递?
-
这个问题其实比较有争议性。对java语言来说他只有值传递。 传递普通类型的时候传递值得副本,传递对象的时候,传的是引用的复制
-
//Java 面向对象三大特点
-
封装:
-
1、隐藏细节 提高代码的安全性
-
2、“高内聚”:封装细节,便于修改内部代码,提高可维护性
-
3、“低耦合”:简化外部调用,便于调用者使用,便于扩展和写作。
-
继承:
-
增加了代码的复用
-
多态:
-
主要是提高了代码的可扩展性。
从重写重载方便说下多态:
-
重写与重载区别:
-
重载(Overload)同时存在于同一个类中,函数名一样,参数个数与类型不一样。(调用方法时通过传递不同参数个数和参数类型来决定具体使用哪个方法,在编译期间就已经确定了,是静态的,所以理论上他不是多态)。
-
重写(Override)存在父类与子类之间,指派生类重写基类的虚函数。重写的函数不但要求函数名参数相同而且还要求返回值类型相同,(父类指针根据赋予给它不同的子类指针动态地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的,因此这样的函数地址是在运行期绑定的)。
-
多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同。
-
通过继承实现多态:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用。没有被重写时,会按照继承关系搜索父类的方法表中该偏移量对应的方法。 其中子类和父类同名的方法共享一个方法项,因此,方法表的偏移量总是固定的。
-
通过接口实现多态:Java 允许一个类实现多个接口,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表。所以从效率上来说,接口方法的调用总是慢于类方法的调用的
-
方法表是实现动态调用的核心。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向记录该类方法的方法表,方法表中的每一个项都是对应方法的指针。这些方法中包括从父类继承的所有方法以及自身重写(override)的方法。
接口与抽象类的区别:
-
1、接口的设计目的,是对类的行为进行约束。而抽象类的设计目的,是代码复用
-
2、接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法,类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现抽类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
-
3、Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
-
//很明显,这个接口很多类实现,如果不是final 类型,那么其中一个类值变了,那么其他的类的值也全乱了。
-
4、Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
-
//很明显,必须是public的,要不然怎么实现它。
-
5、类可以实现很多个接口,但是只能继承一个抽象类
浅拷贝与深拷贝的区别
- 两者都会返回一个对象,只不过不同的地方在于属性: 直接将源对象中的属性的引用值拷贝给新对象的属性字段称为浅拷贝;根据原对象中的属性指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的对象的属性字段叫做深copy。
String ,Stringbuilder,Stringbuffer区别
-
String :字符数组被final 修饰。不能动态改变,线程安全。
-
两个字符串拼接,其实会转为stringbulder,然后调用append 拼接,
-
然后在toString,这个函数会新建String对象然后把Stringbuilder值放进去。
-
Stringbuilder:字符数组没有被final修饰,字符串拼接不会产生新的对象,非线程安全,速度比较快
-
Stringbuffer:字符数组没有被final修饰,字符串拼接不会产生新的对象,线程安全的,但是加的是sychronize重量锁,速度没有Stringbuilder快
Java泛型
-
Java “泛型” 意味着编写的代码可以被不同类型的对象所重用。泛型的提出是为了编写重用性更好的代码。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
-
如果使用Object来实现通用、不同类型的处理,有这么两个缺点:
-
1、每次使用时都需要强制转换成想要的类型
-
2、在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全(classCastException)
-
泛型的类型擦除
-
Java 中的泛型和 C++ 中的模板有一个很大的不同:
-
C++ 中模板的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。
-
Java 中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类
-
当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做类型擦除(type erasure)。这个过程对jvm透明。
-
泛型擦除的实现原理:
-
Java 编辑器会将泛型代码中的类型完全擦除,使其变成原始类型。当然,这时的代码类型和我们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,可以保证类型安全。总之泛型就是一个语法糖,它运行时没有存储任何类型信息。
- class Pair<T> {
- private T value;
- public T getValue() {
- return value;
- }
- public void setValue(T value) {
- this.value = value;
- }
- }
- class Pair {
- private Object value;
- public Object getValue() {
- return value;
- }
- public void setValue(Object value) {
- this.value = value;
- }
- }
-
因为在Pair中,T是一个无限定的类型变量,所以用Object替换。如果是Pair,擦除后,类型变量用Number类型替换。
-
E - Element (在集合中使用,因为集合中存放的是元素)
-
T - Type(表示Java 类,包括基本的类和我们自定义的类)
-
K - Key(表示键,比如Map中的key)
-
V - Value(表示值)
-
N - Number(表示数值类型)
-
? - (表示不确定的java类型)
-
- 无限制通配符
-
要使用泛型,但是不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。
-
? 和 Object 不一样,List<?> 表示未知类型的列表,而 List 表示任意类型的列表。
-
如传入个 List ,这时 List 的元素类型就是 String,想要往 List 里添加一个 Object,这当然是不可以的。
-
- extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
-
3.<? super E> super 关键字声明了类型的下界,表示参数化类型可能是指定类型,或者是此类型的父类。
-
< ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象
-
E get
-
< ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象
-
ArrayList list1 = new ArrayList(); //第一种 情况
-
ArrayList list2 = new ArrayList(); //第二种 情况
-
这样是没有错误的,不过会有个编译时警告。
-
不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。
-
因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行
-
类型擦除与多态的冲突和解决方法,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。