神贴献上:https://www.nowcoder.com/discuss/421353?channel=-1&source_id=1
本文的所有问题和部分答案均来自神贴。
神贴作者总结的知识点:https://blog.csdn.net/qq_33945246/article/details/89922244
一、数据类型:
1. 基本类型
| 四类 | 八种 | 字节数 |
| 整型 | byte | 1 |
| short | 2 | |
| int | 4 | |
| long | 8 | |
| 浮点型 | float | 4 |
| double | 8 | |
| 字符型 | char | 2 |
| 布尔型 | boolean | 1(true, false) |
一个字节有8个比特。整数常量可以根据所在范围来确定类型,例如100,在-128~127之间,所以是byte类型,500在-32768~32767之间,所以是short类型等。java中默认的整数类型是int. 如果是long型的常量需要在数字后面加“L”或“l”,例如“100L”, "12345678910L"。浮点型默认类型为double. float型在后边加“F”,例“1.26F”。
注意:字符串String类型是引用类型,以上都是基本类型。使用方式和基本类型一样。
数据类型转换:
按照数据范围从大到小排序: byte -> short -> int -> long -> float -> double
自动转换:数据范围小的类型可以自动转换为数据范围大的类型
例如,double d = 1000; // 1000是int型,可以直接转成double型
int i = 100;
double d2 = i; //直接转换
强制类型转换:数据范围大的数据类型转成数据范围小的类型,会丢失精度
例如,double d = 3.14;
int i = (int)d; //打印 i 是3
2. 包装类型。 拆箱和装箱分别是什么?分别应用在什么场景?
1. 定义:装箱是将值类型装成引用类型的过程;拆箱就是将引用类型转换成值类型的过程;而每一个基本类型又各自对应了一个引用类型,称为包装类型。
2. 在JDK1.5之前,要生成一个数值为1的Integer对象,需要这样写:
Integer i = new Integer(1);
从JDK1.5开始就提供了自动装箱的功能,要生成一个数值为1的Integer对象,只需:
Integer i = 1;(这个过程中会自动根据数值创建对象的Integer对象,这就是装箱。)
举例:
Integer i = 1; //装箱(定义的时候自动进行装箱)
int j = i; //拆箱(进行复制或者逻辑运算的时候自动进行拆箱)
- int装箱的时候自动调用Integer.valueOf(int)方法;
- Integer拆箱的时候自动调用Integer.intValue方法。
3. 作用:为了保证通用性和提高系统性能
- Java是一种完全面向对象的语言,似乎只需要一种方式来对象这些对象就可以了。但是对于CPU来说,处理一个完整的对象需要很多的指令,对于内存来说需要很多的内存。如果连整数都是对象,性能自然很低。
- 拆箱装箱机制,使得这些基本类型在一般的编程中被当做非对象的简单类型处理,在另一些场合,又允许它们被视作是一个对象。
4. 场景:集合类泛型只能是包装类
3. 缓存池。给定三个变量:i1、i2、i3。Integer i1 = 120;Integer i2 = 120;int i3= 120;i1 和 i2 一样吗?i1 和 i3 呢?为什么?如果把 120 换成 130 呢,i1,12,i3 的关系又如何,为什么?
首先上答案:
- i1 和 i2 一样吗? 答:i1==i2 一样
- i1 和 i3 呢?答:i1==i3,但是i1和i3不一样,因为一个是对象的引用,一个是基本数据类型的值
- 如果把 120 换成 130 呢?答:i1和i2不一样,i1==i3,但却不一样
为什么?
先看一段代码:

当我们给一个Integer赋予一个int类型的时候会自动装箱,调用Integer的静态方法valueOf。
思考:那么Integer.valueOf()返回的Integer是不是都是重新new出来的呢?如果是的话,那么在判断'=='的时候返回的应该都是false才对,因为他们的引用地址会不一样。
具体看一下Integer.valueOf的源码

可知:int数值在-128~127之间的时候,直接从cache中获取,这些cache引用对Integer对象地址是不变的,但是不在这个范围内的数字,就会new Integer(i),地址就会是新的,是不可能一样的。
注意:Integer的默认值是null(对象的默认值都是null) int型默认值是0。
再说说基本类和包装类的==、equals
1. 基本型和包装类型进行"=="运算符的比较,包装类型会自动拆箱变为基本型后再进行比较,因此Integer会自动拆箱为int再比较,显然返回true;
2. 两个Integer类型进行"=="比较,如果其值在-128~127之间,返回true,否则返回false;
3. 两个包装类型进行equals比较,首先会比较类型,如果类型相同,则继续比较值,如果值也相同,则返回true。

4. 包装类型调用equals(),但是参数是基本类型,这时候会先进行自动装箱,基本型转换为其封装类型,再进行3中的比较。
5. 对于equals方法来说,如果没有重写过,就相当于"==",也就是直接比较地址。包装类型都是重写过equals方法的。
二、String
1. String str=“abc” 和 String str=new String (“abc”); 产生几个对象? String str = new String (“hello”)+new String (“123”); 产生了几个对象?
答案可以参考:https://www.cnblogs.com/flyingrun/p/12781257.html ,https://blog.csdn.net/Dongguabai/article/details/85157094
字符串常量池:
1. String str = "abc",执行这句话时,会先去字符串常量池中搜索是否有"abc"这个字符串,如果有,则将字符串首地址赋值给str,如果没有,生成一个新的字符串"abc",并且将首地址赋值给str;
2. String str = new String("abc"),执行这句话时,会在堆里生成一个对象,str存储的是这个对象的地址,同时,
在jdk1.7前:
还会去查看字符串常量池中是否有该字符串,如果有就不处理,如果没有还会在池中生成一个相同的字符串。
在jdk1.7及其以后:
由于常量池已经在堆中,所以new String之后不会再去常量池生成一份字符串。
看两段代码:


new String("hello")时,会产生两个对象,首先是以字面值的方式创建的字符串的对象,是字符串池中的对象,然后会在堆中new出一个对象,s1是指向堆中对象的引用。
执行s1.intern()时会在字符串池中找是否有"hello"这个字符串,发现有,则把这个字符串的引用返回给intern1,所以intern1指向的是池中的字符串对象的引用。
执行String s2 = "hello"时,也会去池中找"hello"字符串并返回字符串引用。
所以s1 != s2; 而intern1 == s2.
当执行String s3 = new String("hello") + new String("hello")时,先去池中找有没有"hello",发现有则不会再以字面量创建池中的对象,只需要在堆中new出两个对象,并且拼接之后的对象"hellohello"也是在堆中的,因为String不可变,是用StringBuilder创建出来的。
当执行String intern3 = s3.intern()时,就有jdk版本的区别了。jdk6会创建字面量"hellohello"这个对象,在池中;jdk7及以后不会创建这个对象,而是把堆中的对象引用加入池中,所以池中的"hellohello"的引用是指向堆中对象的,并返回这个引用。
当执行String s4 = "hellohello"时,就会返回指向堆中对象的引用了。
所以s3 == s4,intern3 == s4.


区别在于是先调用intern还是后调用
1.先运行intern方法:
在jdk1.6的时候,intern能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中,但是现在的版本,intern方法实现的功能是:
- 如果在池中存在这个字符串,则返回字符串;
- 如果不存在相应的字符串,把堆中该字符串的引用加入池中,并返回该引用。
所以在jdk1.7之后,当执行s3.intern()时,jvm不再把s3的字面量复制一份到字符串常量池中,发现池中没有"hellohello",所以把引用加入其中,当运行到String s4 = "hellohello"时,发现池中已经存在一个字面量为"hellohello"指向堆中的引用,则返回这个引用,而这个引用就是s3,所以s3 == s4输出true。
2. 后执行intern方法:
先运行String s4 = "hellohello",在池中没这个字面量或者相应的堆中的引用,所以在池中创建字面量,所以s3 != s4.
然后在执行intern方法的时候,由于池中已经存在该字面量,所以会把这个字面量的引用即首地址返回,所以intern3==s4。
注意:
- String类是被final修饰的,一旦调用方法对String进行修改,就会生成新的对象,对象的引用就会发生变化,指向新的对象。
- String str1=”java”; 的 String 对象放在常量池里,但是它是在编译时那么做的,而 String s=str1+str2; 是在运行时刻才能知道,也就是说 str1+str2 是在堆里创建的。

其实,所说的在字符串常量池中的字符串在运行时也会在堆中创建对象,字符串常量池中存的只是字符串对象的引用。只不过是字符串对象和new String()的对象不是一个,所以引用也不会一样。
2. 字符常量池的位置?字符常量池存储的内容?
在JDK6.0及之前版本,字符串常量池存放在方法区中.
在JDK7.0版本以后,字符串常量池被移到了堆中了。
Java创建字符串对象的两种方式:

第一种方式声明的字面量hellow是在编译期就已经确定的,它会直接进入class文件常量池中;当运行期间在全局字符串常量池中会保存它的一个引用.实际上最终还是要在堆上创建一个”hellow”对象,这个后面会讲。
第二种方式方式使用了new String(),也就是调用了String类的构造函数,我们知道new指令是创建一个类的实例对象并完成加载初始化的,因此这个字符串对象是在运行期才能确定的,创建的字符串对象是在堆内存上。
所以s0和s1所指向的对象都存在于堆上,但地址不同。
全局共享的“字符串常量池”和运行时常量池不是一个概念。class文件里常量池里大部分数据会被加载到“运行时常量池”,包括String的字面量;同时"hello"字符串的引用会被存到字符串常量池中,而"hello"本体和所有对象一样,创建在堆中。
当用字面量赋值的方法创建字符串时,无论创建多少次,只要字符串的值相同,它们所指向的都是堆中的同一个对象。
字符串常量池的本质:
字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
结论:综上,其实字符串常量池中存储的只是堆中用字面量赋值的方法创建的字符串的引用,而通常所说的“进入了字符串常量池的字符串”其实是池中引用所指向的堆中的实例。
原文链接:https://cloud.tencent.com/developer/article/1450501
3. String, StringBuilder, StringBuffer
- 可变性
- String 不可变
- StringBuffer和StringBuilder可变
- 线程安全
- String不可变,因此是线程安全的
- StringBuilder不是线程安全的
- StringBuffer是线程安全的,内部使用synchronized进行同步
- 运算速度(通常情况下):StringBuilder > StringBuffer > String
总结:
- String:适用于少量的字符串操作
- StringBuilder:该类被设计用作StringBuffer的一个简易替换,适用于单线程下字符串缓冲区进行大量操作
- StringBuffer:适用于多线程下字符串缓冲区进行大量操作。
三、运算
1. 参数传递
Java的参数是以值传递的形式传入方法中,而不是引用传递。(不是参数是引用类型,它就是引用传递。)
2. switch
switch (表达式){
case 常量1:
要执行的语句;
break;
case 常量2:
要执行的语句;
break;
...
default:
要执行的语句;
break;
}
switch语句支持的数据类型:
JDK1.0-1.4: byte int short char
JDK1.5: byte int short char enum(枚举)
JDK1.7: byte int short char enum(枚举) String
switch特性:case的穿透性
案例:星期1-5输出工作日,星期6-7输出休息日
当case后面的常量与switch中的表达式匹配后,在遇到break之前,会一直向下穿透
switch(week){
case 1:
case 2:
case 3:
case 4:
case 5:
System.out.println("工作日");
break;
case 6:
case 7:
System.out.println("休息日");
break;
}
四、关键字
final
1. 数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能改变的常量。
- 对于基本类型,final使数值不变;
- 对于引用类型,final使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。

2. 方法
声明方法不能被子类重写。
private方法隐式地被指定为final,如果在子类中定义的方法和基类中的一个private方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
3. 类
声明类不允许被继承。如果类中只有private的构造方法,那么这个类也不可以被继承。单例模式中的构造函数就是私有的,是不可以被继承且通过构造函数声明实例的。
static
1. 静态变量
- 静态变量:又称为类变量,也就是说这个变量是属于类的,类所有的实例都可以共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。

2. 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

静态方法只能访问所属类的静态字段和静态方法,方法中不能有this和super关键字,这两个关键字与具体对象关联。

3. 静态语句块
静态语句块在类初始化时运行一次。

4. 静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。

静态内部类不能访问外部类的非静态的变量和方法。
5. 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

.*的意思是导入这个类里的静态方法。当然,也可以只导入某个静态方法,只要把 .* 换成静态方法名就行了。然后在这个类中,就可以直接用方法名调用静态方法,而不必用ClassName.方法名的方式来调用。
6. 初始化顺序
类的初始化规则:
- 类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
- 超类早于子类和衍生类的初始化
- 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化即使静态域被子类或子接口或者它的实现类所引用。
- 接口初始化不会导致父接口的初始化。
- 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
- 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类
静态成员变量或静态代码块>main方法>非静态成员变量或非静态代码块>构造方法
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
存在继承的情况下,初始化顺序为:
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
volatile
https://www.cnblogs.com/dolphin0520/p/3920373.html
多线程轻量级同步机制,保证可见性和禁止指令重排序,不能保证原子性。
理解这个关键字的作用首先要理解Java内存模型(JMM),线程拥有主存中变量的副本,且多个线程之间共享变量不可见。
最开始为了缓存一致性,在总线上加LOCK锁,一旦一个线程获取了共享变量就加锁,只有释放锁之后其他线程才可以占用,性能不好。
volatile底层实现原理:
变量前加了volatile关键字,线程就会对主内存中的变量值进行嗅探(监听)。
加了这个关键字,会在汇编代码中加入lock指令(汇编指令) --> 1. 重排序 2. 强制修改的值立刻写入主内存中 3. 这个写操作,会触发总线嗅探机制(MESI缓存一致性协议)导致其他的CPU对应的缓存行失效
不能保证原子性的原因是:
书上的解释:volatile只能保证在getfield的时候,inc的值在此时是正确的,但是在执行iconst,putfield这些指令的时候,其他线程可能已经把inc的值改变了。
确实就如同很多人所说的,线程1已经从缓存行读取了inc的值,然后阻塞。此时线程2对inc的值进行了修改,使得线程1的缓存行失效。但是线程1唤醒后是继续往后执行,而不是重新从内存里再get一次。因而就导致了两次+1,只进行了1次。
五、Object通用方法
概览

对象在不重写的情况下使用的是Object的equals方法和hashcode方法,从Object类的源码我们知道,默认的equals判断的是两个对象的引用是否相等,即指向的是不是同一个对象;而hashcode也是根据对象地址生成的一个整数数值。
另外,Object的hashcode()方法的修饰符为native,表明该方法是由操作系统实现,java调用操作系统底层代码获取哈希值。
equals()
== 和 equals 的区别?
- '=='用来比较两个变量(基本类型和对象类型)的值是否相同。对于基本类型,比较的是值,对于对象类型,比较的是对象在栈中的引用,即地址。
- 如果要比较对象的内容是否相同,就需要重写equals方法。要重写的原因是Object类中的equals方法就是用'=='比较的,如果不重写,equals和==是等价的。String是重写了equals方法的。
hashcode()
Object的hashcode方法会根据两个对象的地址生成相应的hashcode,在一些场景下,我们是需要重写hashcode方法的。
例如,在两个对象equals的情况下把它们分别放进Map和Set中,如果没有重写hashcode,会将这两个对象都放进去,因为是通过比较对象的hashcode来判断对象是否一样的,对象的地址不同,hashcode当然会不一样。如果我们想只放进去一个,因为Set有去重操作,那么就要重写hashcode()了,让两个对象的hashcode一样。
原文链接:https://blog.csdn.net/u012557538/article/details/89861552
clone()
1. cloneable
clone()是Object的protected方法,它不是public,一个类不显式去重写clone(),其它类就不能直接去调用该类实例的clone()方法。
Cloneable是标记型接口,它们内部都没有方法和属性,实现Cloneable来表示对该对象能被克隆,能使用Object.clone()方法。如果没有实现Cloneable的类对象调用clone()就会抛出CloneNotSupportedException。
2. 克隆的分类
- 浅拷贝:指拷贝对象时仅仅拷贝对象本身和对象中的基本变量,而不拷贝对象包含的引用指向的对象,即拷贝者中的引用所指向的对象与拷贝者中引用所指向的对象是同一个对象。
- 深拷贝:不仅拷贝对象本身,而且拷贝对象包含的引用所指向的所有对象。
类可见表示其他类可以用这个类创建实例对象。
成员可见表示其它类可以用这个类的实例对象访问到该成员。
protected用于修饰成员或方法,表示在继承体系中成员或方法对于子类可见,但是这个访问修饰符对于类没有意义。
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替。
字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。
抽象类与接口
1. 抽象类
抽象类和抽象方法都使用abstract关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。

抽象类(我的总结):
- 抽象类中不一定有抽象函数
- 有抽象函数的类一定要是抽象类
- 抽象类可以有构造函数,只是不能直接创建抽象类的实例对象。作用是1. 不管父类是不是抽象类,构造子类实例的时候,就会调用父类构造函数;2. 用于初始化一些通用变量,继承它的子类可以使用。
- 继承抽象类可以是普通类:必须重写抽象类中所有的抽象方法;继承抽象类的也可以是抽象类:无需重写抽象类中的所有抽象方法。
2. 接口
接口是抽象类的延伸,在Java8之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
从Java8开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在Java8之前,如果一个接口想要添加新的方法,那么要修改所有实现了改接口的类,让它们都实现新增的方法。
接口的成员(字段+方法)默认都是public的,并且不允许定义为private或protected。
接口的字段默认都是static和final的。

接口(我的总结):
- jdk1.8之前,是完全抽象的抽象类,只有方法的声明,没有方法体;jdk1.8及其以后,允许有default修饰的默认非抽象方法,也可以声明静态方法,并且可以实现。
- 成员默认是public,不能是private,protected(因为没有意义,接口是用来被实现的,接口主要是定义了规则);字段默认且只能是public static final常量
- 不可以有构造函数
- 子类实现接口必须对接口的抽象方法重写
3. 比较
- 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
看看这个:https://blog.csdn.net/YuanMxy/article/details/74389953
Java 8 对接口做了进一步的增强。
a. 在接口中可以添加使用 default 关键字修饰的非抽象方法。即:默认方法(或扩展方法)
b. 接口里可以声明静态方法,并且可以实现。
更多的比较见:https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/
super
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super() 函数。
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。



重写与重载
1. 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里式替换原则,重写有以下三个限制:
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中:
- 子类方法访问权限为 public,大于父类的 protected。
- 子类的返回类型为 ArrayList,是父类返回类型 List 的子类。
- 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
- 子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。

在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有再到父类中查看,看是否从父类继承来。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:


2. 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
多态
多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
对面向对象来说,多态分为编译时多态和运行时多态。其中编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性。
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
关于多态的问题,可以大体概括为:编译看左边,运行看右边。意思编译时候,看左边有没有该方法,运行的时候结果看 new 的对象是谁,就调用的谁。

当调用 say 方法执行的是 Son 的方法,也就是重写的 say 方法,而当调用 action 方法时,执行的是 father 的方法。
-
普通方法,运用的是动态单分配,是根据 new 的类型确定对象,从而确定调用的方法;
-
静态方法,运用的是静态多分派,即根据静态类型确定对象,因此不是根据 new 的类型确定调用的方法
Java 实现多态有 3 个必要条件:继承、重写和向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
七、反射
概述
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。
实际上,我们创建的每一个类也都是对象,即类本身是java.lang.Class类的实例对象,这个实例对象称之为类对象,也就是Class对象。
如图是类的正常加载过程,加载完每一个类都会产生相应的class对象,相同的class产生的对象是唯一的。反射的原理在于class对象。

获取对象的三种方式
- 通过Object类的getClass()函数,由于Object是根类,每一个类都有这个函数
- 每一个类(包括基本数据类型,注意这里基本数据类型不用转成包装类)都有一个class属性,静态属性,通过类名直接访问
- 通过Class类的静态方法forName(String className)
package test; public class Solution { public static void main(String[] args) { TreeNode tree = new TreeNode(0); // 第一种方式 Class t1 = tree.getClass(); System.out.println(t1.getName()); // 第二种方式 Class t2 = TreeNode.class; System.out.println(int.class.getName()); System.out.println(t1 == t2); // 第三种方式 try { Class t3 = Class.forName("test.TreeNode"); System.out.println(t1 == t3); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
结果:

应用场景:反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
举例看神贴中的“十分钟搞懂系列”。
更多信息:https://www.sczyh30.com/posts/Java/java-reflection-1/
八、异常
异常分类
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。
- 错误:Error类以及他的子类的实例,代表了JVM本身的错误,不能被程序员通过代码处理。程序员应该关注Exception为父类的分支的各种异常类。
- 异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
异常与错误的区别:异常可以通过程序自身捕捉处理,而错误是程序自身无法处理的。
- 受检异常:编译器要求必须处理的异常,这类异常的发生在一定程度上是可以预计的,而且这类异常一旦发生,就必须采用某种方式进行处理。除了RuntimeException及其子类以外的其它异常类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,出现这种异常,要么用try-catch语句捕捉它,要么用throws语句声明抛出它,否则编译不通过。
- 非受检异常:编译器不要求强制处理的异常,包括运行时异常(RuntimeException与其子类)和Error。
异常处理机制
一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。异常总是先被抛出,后被捕捉的。
Java规定:对于受检异常必须捕捉、或者声明抛出。允许忽略不可查的RuntimeException和Error。
异常处理语法
异常处理
1. 运行时异常
出现运行时异常后,系统会把异常一直往上抛,一直遇到处理代码。如果没有遇到处理块,到最上层,如果是多线程就由Thread.run()抛出,线程中止;如果是单线程就被main()抛出,主程序中止。运行时异常是Exception的子类,也有一半异常的特点,是可以被Catch块处理的,只是往往不对它处理。也就是说,如果不对运行时异常进行处理,出现异常时会线程中止或主程序中止。
2. 编译异常(受检异常)
在编写代码处理异常时,对于检查异常,有2种不同的处理方式:
- 使用try...catch...finally语句块处理它。
- 在函数签名中使用throws声明交给函数调用者caller去解决。
实例:
-
A方法存在异常风险,然后main方法调用A方法
情况:
-
情况1:A使用try-catch处理了异常,则main方法不需要做额外处理
-
情况2:A在方法签名中throws了异常,此时main需要做出处理,有两种方式,第一就是使用try catch 捕获异常,另一种是在main方法签名中throws异常,jvm调用main方法,这是最后一层处理异常的机制了
具体语法
1. try catch finally处理
- try块中的局部变量,catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
- 每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个 try 块下的多个 catch 异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个 catch 块都有存在的意义。
- Java让执行流回复到处理了异常的catch块后接着执行,try块中的异常后面的代码就不会在执行。这种策略叫:termination model of exception handling(终结式异常处理模式)
注:运行时异常不加处理语句的时候,异常出现之后程序就结束了,但是使用try catch捕获异常之后,程序并没有结束,而是在catch语句之后继续执行。
2. throw 和 throws 的区别?
- throw用于方法内部,throws用于方法声明上
- throw后跟异常对象,throws后跟异常类型
- throw后只能跟一个异常对象,throws后可以一次声明多种异常类型
异常处理机制:异常处理是对可能出现的异常进行处理,以防止程序遇到异常时被卡死,处于一直等待,或死循环。异常有两个过程,一个是抛出异常;一个是捕捉异常。
系统自动抛出的异常:所有系统定义的编译和运行异常都可以由系统自动抛出,称为标准异常,并且Java强烈地要求应用程序进行完整的异常处理,给用户友好的提示,或者修正后使程序继续执行。
当程序语句出现一些逻辑错误、主义错误或类型转换错误时,系统会自动抛出异常。如:

系统会自动抛出ArithmeticException异常:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at test.ExceptionTest.main(ExceptionTest.java:62)
语句抛出的异常:用户程序自定义的异常和应用程序特定的异常,必须借助于throws和throw语句来定义抛出异常。
1. throws关键字
定义一个方法的时候可以使用throws关键字声明。使用throws关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理。使用throws抛出的运行时异常相当于提示调用者,此方法有风险,可能会抛出这些异常,至于是否处理该风险由调用者决定。
格式:public 返回值类型 方法名称 (参数列表,,)throws 异常类 {};
2. throw关键字
程序员可以通过throw语句手动显式地抛出一个异常,throw语句后面必须是一个异常对象。throw语句必须写在函数中,执行throw语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。在异常处理中,try语句要捕获的是一个异常对象,那么此异常对象也可以自己抛出。
原文链接:https://blog.csdn.net/luoweifu/java/article/details/10721543
异常注意事项
1. throws方法的重写
当子类重写父类的带有 throws 声明的函数时,其 throws 声明的异常必须在父类异常的可控范围内 —— 用于处理父类的 throws 方法的异常处理器,必须也适用于子类的这个带 throws 方法 。这是为了支持多态。
例如,父类方法 throws 的是 2 个异常,子类就不能 throws 3 个及以上的异常。父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。
2. try catch finally中的return
-
在 try 块中即便有 return,break,continue 等改变执行流的语句,finally 也会执行。
-
try…catch…finally 中的 return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是 0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。finally 中的 return 会覆盖 try 或者 catch 中的返回值。
-
finally 中的异常会覆盖(消灭)前面 try 或者 catch 中的异常
例1
package test; public class Solution { public static String m1(){ try{ System.out.println("this is try method"); return "this is try return"; }catch(Exception e){ e.printStackTrace(); }finally{ System.out.println("this is finally method"); return "this is finally return"; } } public static void main(String[] args) { System.out.println(m1()); } }
结果:try中即使有return语句finally块也会执行,且finally中的return会覆盖try或catch中的返回值。

例2:
package test; public class Solution { public static String m1(){ try{ System.out.println("this is try method"); return "this is try return"; }catch(Exception e){ e.printStackTrace(); }finally{ System.out.println("this is finally method"); // return "this is finally return"; // 三个return 同时出现会编译错误 } return "this is last return"; } public static void main(String[] args) { System.out.println(m1()); } }
结果:在try块中就已经return了,且finally中的return和方法中的return不能共存。

总结:
-
finally 中的 return 和方法的 return 不能共存,因为 finally 是异常处理的最后执行的部分,finally必定执行,内部的return跟方法最后的return存在冲突。
-
当方法和try语句块都有return的时候,不会执行方法的return,因为finally是最后执行部分,先执行try语句,然后执行finally语句,最后执行try的return语句。try..catch不一定执行,所以内部return可以跟方法的return共存(可以参照if条件判断)
九、泛型
出现泛型的原因(背景):
Java推出泛型以前,程序员可以构建一个元素类型为Object的集合,该集合能够存储任意的数据类型对象,而在使用该集合的过程中,需要程序员明确知道存储每个元素的数据类型,否则很容易引发ClassCastException异常。
例:
package test; import java.util.ArrayList; public class Solution { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("java"); list.add(100); for(int i = 0; i < list.size(); i++){ String str = (String)list.get(i); System.out.println(str); } } }

是在运行时期发生的异常。
当然在数据量小的时候并在明确知道每个元素的数据类型时,我们可以把每一个元素都强制类型转换一下,但这样很麻烦。
泛型的概念
Java泛型是JDK5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许我们在编译时监测到非法的类型数据结构。
泛型本质就是参数化类型,也就是所操作的数据类型被指定为一个参数。
1. 类型安全
2. 消除了强制数据类型转换

已经明确存储的就是String类型,如果添加其他类型元素就会报错。泛型集合提供给了编译器的类型检测机制,获取集合元素时也不需要类型转换。
泛型类
常用泛型标识:T、E、K、V(K,V一般会结合用于例如哈希表中的键值对)

在外部使用时来指定类型,例:Box<String> box = new Box<String>("box");
1. 泛型类在创建对象的时候,没有指定类型,将按照Object类型来操作。
2. 泛型类,不支持基本数据类型,基本数据类型不是继承自Object类型。
3. 同一泛型类,根据不同的数据类型创建的对象,本质上是同一Class类型,即getClass()是一样的。
从泛型类派生子类
- 子类也是泛型类,子类和父类的泛型类型(泛型标识)要一致 class ChildGeneric<T> extends Generic<T>
- 子类不是泛型类,父类要明确泛型的数据类型 class ChildGeneric extends Generic<String>
泛型接口
- 实现类不是泛型类,接口要明确数据类型
- 实现类也是泛型类,实现类和接口的泛型类型要一致
泛型方法
- 泛型类,是在实例化的时候指明泛型的具体类型
- 泛型方法,是在调用方法的时候指明泛型的具体类型
语法:
修饰符 <T, E, ...> 返回值类型 方法名(形参列表) {
}
例:public <T> T getProduct(ArrayList<T>){}
- public与返回值之间的<T>非常重要,可以理解为声明此方法为泛型方法
- 只有声明了<T>的方法才是泛型方法,要注意和泛型类中的成员方法(不是泛型方法)进行区分
- 若泛型方法在泛型类中,要注意泛型方法的类型是独立于泛型类的泛型类型的,即使标识使用的是同一个
类型通配符:一般是使用"?"代替具体的类型实参,代表任意类型
例:
Box泛型类:

测试程序1:

测试程序2:

正确做法:

类型通配符的上限

类型通配符的下限

类型擦除
概念:泛型是Java1.5才引进的概念,之前是没有泛型的,但是,泛型代码能够很好地和之前版本的代码兼容。因为泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为类型擦除。


发现运行之后输出的类别信息中没有泛型相关信息。
无限制类型擦除:编译之后直接把类型标识转变成Object类型



有限制类型擦除:改一下Box类,再跑一遍测试代码,会把传入的类型擦除为上限Number类型


泛型方法也同样适用:
(在Box类中)

(在测试类中)


桥接方法

接口:

实现类:

测试程序:

结果:多了个Object类型的info方法,因为接口类型擦除之后info方法就是Object类型。达到对接口方法的重写,桥接方法是编译器替我们做的,为了保持接口和类的实现关系。

10道Java泛型面试题:https://cloud.tencent.com/developer/article/1033693
十、注解
概念:JDK1.5之后的新特性,用来说明程序的,给计算机看的;注释是给程序员看的。、
作用分类:
- 编写文档:通过代码里标识的元数据(注解)生成文档【生成文档doc文档】
- 编译检查:通过代码里的标识的元数据(注解)让编译器能够实现基本的编译检查【Override】
- 代码分析:通过代码里标识的元数据(注解)进行分析【使用反射】——可自定义
编写文档示例:注意Javadoc注释写法为: /** 注释内容 */ (在敲完“/**”之后直接回车,跟java注释/* 注释内容*/ 是不一样的,而且颜色也不同)

将这段代码复制到文档中,用命令行javadoc ***.java即可。注意,如果有中文,注意文档的编码格式

打开其中生成的index.html,就是下面这样的

学习内容:
- JDK中预定义的一些注解
- @Override:检测被该注解标注的方法是否是继承自父类(接口)的,比较安全
- @Deprecated:该注解标注的内容,表明已过时,建议使用新版。
- @SuppressWarnings:抑制编译器警告,需要传参,一般传"all"。
- 自定义注解
- 格式:
元注解
public @interface 注解名称{}
例:public @interface MyAnno{}
-
- 本质:注解本质上是一个接口,该接口默认继承Annotation接口
public interface MyAnno extends java.lang.annotation.Annotation {}
-
- 属性:接口中可以定义的成员方法
- 要求:
- 属性的返回值类型有下列取值,例如,不可以是void或者类类型
-
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
- 属性:接口中可以定义的成员方法
2. 定义了属性,在使用时需要给属性赋值
-
-
-
- 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
- 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
- 注解类型赋值,anno=@MyAnno;数组赋值,值使用{}包裹,如果数组中只有一个值,{}可以省略。
-
-
例,定义注解

使用注解

或者赋予默认值,就不用在使用的时候赋值了

-
- 元注解:用于描述注解的注解
- @Target:描述注解能够作用的位置,类or方法or成员变量
- @Retention:描述注解被保留的阶段,SOURCE/CLASS/RUNTIME,一般会用RUNTIME,被描述的注解会保留到class字节码文件中,并被JVM读取到。
- @Documented:描述注解是否被抽取到api文档中
- @Inherited:描述注解是否被子类继承,如果一个类被Inherited注解的注解,则其子类会自动继承这个注解
- 元注解:用于描述注解的注解
- 在程序使用(解析)注解:获取注解中定义的属性值
- 获取注解定义的位置的对象(Class对象,Method对象或者Field对象)
- 获取指定的注释,getAnnotation(Class),其实就是在内存中生成了一个该注解接口的子类实现对象
- 调用注解中的抽象方法获取配置的属性值
自定义的注解:

类:

测试
package test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @MyAnno(className="test.Demo1", methodName="show") public class Solution { public static void main(String[] args) throws Exception{ //1.解析注解,拿到注解中配置的属性的值 //1.1获取该类的字节码文件对象 Class<Solution> solClass = Solution.class; //2.获取上边的注解对象 //其实就是在内存中生成了一个该注解接口的子类实现对象 /* * public class AnnoImpl implements MyAnno{ * public String className(){ * return "test.Demo1"; * } * public String methodName(){ * return "show"; * } * } */ MyAnno anno = solClass.getAnnotation(MyAnno.class); //3.调用注解对象中定义的抽象方法,获取返回值 String className = anno.className(); String methodName = anno.methodName(); System.out.println(className); System.out.println(methodName); //加载该类进内存 Class cls = Class.forName(className); //创建对象 Object obj = cls.newInstance(); //获取方法对象 Method method = cls.getMethod(methodName); //执行方法 method.invoke(obj); } }
输出:

十、Java序列化
1. 什么是Java序列化
java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;
Class User implements Serializable{ private String username; private String passwd; }
java序列化主要有两个接口:Serialize和Externalizable接口
(1)使用Serializable接口实现序列化(重点,要牢记)



当我们运行序列化方法的时候,发现把数据存在了当前目录的"serialize"文件中,同时运行反序列化方法的之后,可以看到反序列化成功,输出了存储在对象中的值。
(2)使用Externalizable接口实现序列化
Externalizable和Serializable接口的区别:
-
Externalizable继承自Serializable接口
-
需要我们重写writeExternal()与readExternal()方法
-
实现Externalizable接口的类必须要提供一个public的无参的构造器。
import java.io.*; public class User1 implements Externalizable{ private int age; private String name; public User1(String name, int age){ this.name = name; this.age = age; } public User1() { // TODO Auto-generated constructor stub } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { // TODO Auto-generated method stub name = (String)in.readObject(); age = in.readInt(); } @Override public void writeExternal(ObjectOutput out) throws IOException { // TODO Auto-generated method stub out.writeObject(name); out.writeInt(age); } public String toString(){ StringBuilder str = new StringBuilder(); str.append(name); str.append(" "); str.append(String.valueOf(age)); return str.toString(); } }
import java.io.*; public class Solution { public static void main(String[] args) throws IOException, ClassNotFoundException{ ExternalUser(); DeExternalUser(); } //序列化方法 private static void ExternalUser() throws FileNotFoundException, IOException{ User1 user = new User1("External序列化", 23); //序列化对象到文件中 FileOutputStream file = new FileOutputStream("serialize"); ObjectOutput oos = new ObjectOutputStream(file); user.writeExternal(oos); oos.close(); System.out.println("序列化对象成功"); } //反序列化方法 private static void DeExternalUser() throws FileNotFoundException, IOException, ClassNotFoundException{ User1 newuser = new User1();//这应该就是要添加一个无参构造函数的原因 File file = new File("serialize"); ObjectInput ois = new ObjectInputStream(new FileInputStream(file)); newuser.readExternal(ois); System.out.println("反序列化成功"+ newuser.toString()); } }
2. 序列化有什么用
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中; 、
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;
3. 深入分析java序列化机制
(1)serialVersionUID的作用:目的是序列化对象版本控制,有关各版本反序列化时是否兼容。如果我们不设置UID,Java会自动根据类名、接口名、成员方法及属性等来生成一个,如果后面增加或者减少了某个属性,在反序列化的时候生成的UID和序列化时的会不一样,就会报异常。
(2)如果希望某些变量不序列化,该怎么做
在一个类上实现了序列化的接口,整个类下面的属性和成员变量都可以被序列化。
不被序列化的方法:
1. 变量声明成静态变量 static
2. 变量声明成瞬态变量 transient,在被反序列化后,transient变量的值被设为初始值,如int型是0,对象型是null。
例,private transient String passwd;
序列化之前:
username zhangsan;
passwd 1234
反序列化:
username zhangsan
passwd null
十一、Java1.8新增特性
https://www.cnblogs.com/xuxinstyle/p/11447820.html
Java与c++的区别
- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
JRE和JDK
- JRE:Java Runtime Environment,Java 运行环境的简称,为 Java 的运行提供了所需的环境。它是一个 JVM 程序,主要包括了 JVM 的标准实现和一些 Java 基本类库。
- JDK:Java Development Kit,Java 开发工具包,提供了 Java 的开发及运行环境。JDK 是 Java 开发的核心,集成了 JRE 以及一些其它的工具,比如编译 Java 源码的编译器 javac 等。
- 如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
Java创建对象的5中方法:https://blog.csdn.net/Developlee/article/details/101191771

Java程序编译和运行的过程:https://www.cnblogs.com/qiumingcheng/p/5398610.html
内部类:https://www.cnblogs.com/ldl326308/p/9477566.html
匿名内部类举例:https://www.cnblogs.com/nerxious/archive/2013/01/25/2876489.html
总结:
(1)把类定义在另一个类的内部,该类就被称为内部类。
举例:把类B定义在类A中,类B就被称为内部类。
(2)内部类的访问规则
A:可以直接访问外部类的成员,包括私有
B:外部类要想访问内部类成员,必须创建对象
(3)内部类的分类
A:成员内部类
(4)成员内部类访问规则
成员内部类不是静态的:
外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
成员内部类是静态的:
A:局部内部类访问局部变量必须加final修饰。
B:为什么呢?
因为局部变量使用完毕就消失,而堆内存的数据并不会立即消失。
所以,堆内存还是用该变量,而改变量已经没有了。
为了让该值还存在,就加final修饰。
通过反编译工具我们看到了,加入final后,堆内存直接存储的是值,而不是变量名。
(6)匿名内部类(掌握)
A:是局部内部类的简化形式
B:前提
存在一个类或者接口
C:格式:
new 类名或者接口名() {
重写方法;
}
D:本质:
其实是继承该类或者实现接口的子类匿名对象
浙公网安备 33010602011771号