一.基础概念
JVM , JDK,JRE的区别
-
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
-
JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
-
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。
字节码
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
注意: .class -> 机器码
这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(just-in-time compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
Java和C++的区别
-
都是面向对象的语言,都支持封装、继承和多态
-
Java 不提供指针来直接访问内存,程序内存更加安全
-
Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是可以继承多个接口。
-
Java 有自动内存管理垃圾回收机制(GC:Garbage Collection),不需要程序员手动释放无用内存。
Java 创建对象的几种方式
-
使用new关键字
-
使用 newInstance 方法来创建
-
使用反射来创建对象
-
用 clone 方法创建对象
-
使用反序列化创建对象
二.基本语法
接口和抽象类的区别
接口 :使用interface关键字修饰,抽象方法的集合。如果一个类实现了某个接口,那么它就要重写接口的抽象方法。
抽象类:被abstract关键字修饰,不能被实例化,只能被继承,抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。如果一个类继承一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。
-
接口中只能存放 抽象方法
-
抽象类只能继承一个,而接口看可以实现多个
-
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final (常量)的
类的实例化过程
简述final作用
最终的
-
修饰类 : 表示类不可被继承
-
修饰方法 :表示方法不可被子类重写,但是可以重载
-
修饰变量 :表示变量一旦被赋值就不可以更改它的值
-
修饰成员变量:
-
如果final修饰类变量(static 变量),只能在静态初始块中指定初始值或者声明该类变量时指定初始值。
-
如果final修饰成员变量,可以在非静态初始块、声明该变量或者构造器中执行初始值。
-
-
修饰局部变量
系统不会为局部变量进行初始化。
局部变量只声明没有初始化,不会报错,但在使用前必须赋值。
-
修饰基本数据类型和引用数据类型的变量
-
修饰基本数据类型:其数值一旦在初始化后便不能更改。
-
修饰引用数据类型:对其初始化后便不能再让其指向另一个对象,但引用值可变。
final int[] arr = {1,2,3,4}; arr[2] = -3; //合法 arr = null; // 非法
-
-
为什么 局部内部类 和 匿名内部类 只能访问 局部final变量?
匿名内部类:一个接口/类的方法的某个实现方式在程序中只会执行一次,但为了使用它,我们需要创建它的实现类/子类去实现/重写。此时可以使用匿名内部类的方式,可以无需创建新的类,减少代码冗余。
内部类编译之后,会生成两个Class文件。
内部类与外部类 处于 同一级别。内部类不会因为定义在外部类的方法中就会随着方法的执行完毕而被销毁。
-
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有在没人引用它时,才会死亡)。这就导致了一个矛盾:内部类对象访问了一个不存在的变量。
-
为了解决这个问题,将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的“copy”。
-
但将将局部变量复制一份作为内部类的成员变量时,必须保证这两个变量一致。所以 用final修饰局部变量,对其初始化后,就不可更改这个变量,保证了内部类的成员变量和外部方法的局部变量的一致性。
-
class outClass{ // 局部内部类 private int age = 12; public void outPrint(final int x){ class inClass{ public void inPrint(){ sout(x); sout(age); } } new inClass().inPrint(); } } class Test{ psvm; void test(final int b){ //局部final变量a,b final int a = 10; new Thread(new Runnable() { //匿名内部类 @Override public void run() { System.out.println(a); System.out.println(b); } }).start(); } }
字符常量和字符串常量的区别?
-
形式 : 字符常量 是单引号引起的一个字符,字符串常量 是双引号引起的 0 个或若干个字符
-
含义 : 字符常量 相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量 代表一个地址值(该字符串在内存中存放位置)
-
占内存大小 : 字符常量 只占 2 个字节; 字符串常量 占若干个字节 (注意:
char
在 Java 中占两个字节)
this 关键字
this.属性名称 :指的是访问类中的成员变量,用来区分成员变量和局部变量(重名问题)
private String name ;//类中的成员变量 //没加this前,区分不了是 类中的成员变量,还是局部变量 public void setName(String name){ name = name; } //加了this就可以区分了 public void setName(String name){ this.name = name; }
可变长参数
所谓 可变长参数 就是允许在调用方法时传入不定长度的参数。且 可变长参数只能作为函数的最后一个参数,可变长参数 编译后实际会被转换成一个数组。
public static void method2(String arg1, String... args) { //...... }
标识符和关键字
-
标识符:程序的名字
-
关键字:被赋予特殊含义的标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方
自增/自减运算符
++ 和 -- 运算符可以放在变量之前,也可以放在变量之后
-
当运算符放在变量之前时(前缀),先自增/减,再赋值;
-
当运算符放在变量之后时(后缀),先赋值,再自增/减。
continue、break 和 return 的区别
-
continue
:指跳出当前的这一次循环,继续下一次循环。 -
break
:指跳出整个循环体,继续执行循环下面的语句。 -
return
用于跳出所在方法,结束该方法的运行。return 一般有两种用法:-
return;
:直接使用 return 结束方法执行,用于没有返回值函数的方法 -
return value;
:return 一个特定值,用于有返回值函数的方法
-
静态方法为什么不能调用非静态成员
这个需要结合 JVM 的相关知识,主要原因如下:
-
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。
而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
-
在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
静态方法和实例方法的区别
-
调用方式:调用静态方法可以无需创建对象 。
-
静态方法:可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式 -
实例方法:只可以使用
对象.方法名
的方式
-
-
访问限制:
-
静态方法:在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法)
-
实例方法:没有限制
-
重载和重写的区别
-
重载:发生在同一个类中,除了方法名要相同,其他都要不同。就是同样的一个方法能够根据输入数据的不同,做出不同的处理。
-
重写:发生在父子类中,方法名、参数列表必须相同。如果父类方法修饰符为private,子类就不能重写该方法。就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法。
@overwrite
发生在运行期,方法名、参数列表必须相同。
==和equals()的区别
== 对比的是 栈中的值,基本数据类型是变量值,引用数据类型是堆中内存对象的地址
-
==
对于基本类型和引用类型的作用效果是不同的:-
对于基本数据类型来说,
==
比较的是值。 -
对于引用数据类型来说,
==
比较的是对象的内存地址。
-
-
equals()
只能用于判断引用数据类型是否相等。equals()
方法存在于Object
类中,而Object
类是所有类的父类。equals()
方法存在两种使用情况:-
类没有重写
equals()
方法 :通过equals()
比较该类的两个对象时,等价于通过“==”比较这两个对象,即 比较的是对象的引用地址,使用的默认是Object
类equals()
方法。 -
类重写了
equals()
方法 :一般我们都覆盖equals()
方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
-
hashCode() 与 equals()
-
hashCode():定义在
Object
类中,这就意味着 Java 中的任何类都包含有hashCode()
函数。作用是 获取哈希码(int
整数) 。这个哈希码的作用是确定该对象在哈希表中的索引位置。哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了哈希码!(可以快速找到所需要的对象)
-
为什么要有hashCode():
-
以“
HashSet
如何检查重复”为例子来说明为什么要有hashCode
?当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是 如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。*这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。-
其实,
hashCode()
和equals()
都是用于比较两个对象是否相等。但在一些容器(比如HashMap
、HashSet
)中,有了hashCode()
之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HastSet
的过程)! -
为什么不只提供
hashCode()
方法呢?这是因为两个对象的
hashCode
值相等并不代表两个对象就相等。那为什么两个对象有相同的
hashCode
值,它们也不一定是相等的?因为
hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的hashCode
)。总结下来就是 :
-
如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。(所谓哈希碰撞也就是指的是不同的对象得到相同的
hashCode
) -
如果两个对象的
hashCode
值相等并且equals()
方法返回true
,我们才认为这两个对象相等。 -
如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
-
-
-
-
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的
hashCode
值必须是相等。也就是说如果equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等。如果重写
equals()
时没有重写hashCode()
方法的话就可能会导致equals
方法判断是相等的两个对象,hashCode
值却不相等。
三.基本数据类型
Java 中的几种基本数据类型
Java 中有 8 种基本数据类型,分别为:
-
6 种数字类型 :
byte
、short
、int
、long
、float
、double
-
1 种字符类型:
char
-
1 种布尔型:
boolean
。
这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本类型 | 位数(b) | 字节(B) | 默认值 |
---|---|---|---|
int | 32 | 4 | 0 |
short | 16 | 2 | 0 |
long | 64 | 8 | 0L |
byte | 8 | 1 | 0 |
char | 16 | 2 | 'u0000' |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
boolean | 1 | false |
注意:
-
Java 里使用
long
类型的数据一定要在数值后面加上 L,否则将作为整型解析。 -
char a = 'h'
:单引号,String a = "hello"
:双引号。 -
所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
这八种基本类型都有对应的包装类分别为:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
。
包装类型不赋值就是 Null
,而基本类型有默认值且不是 Null
。
另外,这个问题建议还可以先从 JVM
层面来分析。
基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。
String 属于引用数据类型,是个类。
重点: 经常需要改变字符串内容时,使用 StringBuffer 和 StringBuilder ; 多线程使用共享变量时使用StringBuffer
-
String 是 final 修饰的,不可变,每次操作都会产生新的 String对象
-
StringBuffer 和 StringBuilder 都是在原对象上操作
String.format(); :方便格式化文本
如:String s = String.format("%s今年%d岁。", "小李", 30); // "小李今年30岁。"
占位符 "%" 后面的字母决定了其接受的实际参数的类型。
包装类型的常量池技术
Java 基本类型的包装类的大部分都实现了常量池技术。
-
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据 -
Character
创建了数值在 [0,127] 范围的缓存数据 -
Boolean
直接返回True
orFalse
-
两种浮点数类型的包装类
Float
,Double
并没有实现常量池技术。 -
例子:下面的代码的输出结果是
true
还是flase
呢?
Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);
Integer i1=40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40)
。因此,i1
直接使用的是常量池中的对象。而Integer i2 = new Integer(40)
会直接创建新的对象。
因此,答案是 false
。
自动装箱与拆箱的原理
-
什么是自动拆装箱?
-
装箱:将基本类型用它们对应的包装类型包装起来;
-
拆箱:将包装类型转换为基本数据类型;
-
Integer i = 10; //装箱 int n = i; //拆箱
-
-
原理:
装箱其实就是调用了 包装类的
valueOf()
方法,拆箱其实就是调用了xxxValue()
方法。因此,
-
Integer i = 10
等价于Integer i = Integer.valueOf(10)
-
int n = i
等价于int n = i.intValue()
;
-
内部类
1. 什么是内部类
答:将一个类的定义放在里另一个类的内部,将内部类分为四种:成员内部类、静态内部类、局部(方法)内部类、匿名内部类。
/** * 我是一个外部类(外部是相对内部而言) */ public class Outer{ /** * 我是一个内部类 */ class Inner{ //... } }
2. 为什么要用内部类?
答:每个内部类都能独立地继承一个类,所以无论外部类是否已经继承了某个类,对于内部类都没有影响。
曹氏解读:java使用内部类就是为了实现多继承的效果。
其中,匿名内部类也就是没有名字的内部类,正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写
但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口
一个接口/类的方法的某个实现方式在程序中只会执行一次,但为了使用它,我们需要创建它的实现类/子类去实现/重写。此时可以使用匿名内部类的方式,可以无需创建新的类,减少代码冗余。
3. 如何使用内部类
其中,匿名内部类使用实例:
public interface IAnimal{ //定义一接口 void speak(); } public class Outer { public static IAnimal getInnerInstance(String speak){ return new IAnimal(){ //匿名内部类格式: new 外部接口(){@Override} @Override public void speak(){ System.out.println(speak); }}; //注意上一行的分号必须有 } public static void main(String[] args){ //调用的speak()是重写后的speak方法。 Outer.getInnerInstance("小狗汪汪汪!").speak(); } }
匿名内部类使用格式
new 接口/类名(参数1, 参数2...){ 实现方法1(){ } 实现方法2(){ } ...... };
常用匿名内部类 创建并启动线程
new Thread(new Runnable() { @Override public void run() { System.out.println("我是一个线程"); } }).start();
Java中的不可变类
-
不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。Java 中八个基本类型的包装类和 String 类都属于不可变类,而其他的大多数类都属于可变类。
-
String 是 被
final
和private
关键字修饰,不可变,每次操作都会产生新的 String对象
-
-
不可变类的优点 :线程安全
-
不可变类的设计方法 :类添加final修饰符;
四.面向对象基础
面向对象和面向过程的区别
-
面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
-
面向对象:会先抽象出对象,然后用对象执行方法的方式解决问题。
成员变量与局部变量的区别
-
语法形式 :从语法形式上看,成员变量是属于类或实例的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。 -
存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例对象的。而实例对象存在于堆内存,局部变量则存在于栈内存。 -
生存时间 :从变量在内存中的生存时间上看,成员变量是实例对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
-
默认值 :从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。而一个对象引用可以指向 0 个或 1 个对象
一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗?
-
构造方法主要作用是完成对类对象的初始化工作。
-
如果一个类没有构造方法,也可以执行!因为会有默认的无参构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,这时候,就不能直接 new 一个对象而不传递参数了,如:
Confun c1=new Confun();
。所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。
构造方法有哪些特点?是否可被重写?
-
构造方法特点如下:
-
名字与类名相同。
-
没有返回值,但不能用 void 声明构造函数。
-
生成类的对象时自动执行,无需调用。
-
-
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
面向对象的三大特征
-
封装
封装是指把一个对象的属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
-
继承
不同类型的对象,经常有一些共同点。例如,小明同学、小红同学,都是学生。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱。继承是使用已存在的类的定义作为基础 去建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能只继承父类的一部分。通过使用继承,可以快速地创建新的类,可以提高代码的重用率。
关于继承如下 3 点请记住:
-
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
-
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
-
子类可以用自己的方式实现父类的方法。
-
-
多态 :表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例
1. 多态就是 同一个接口,使用不同的实例而执行不同操作,如图所示:
-
多态存在的三个必要条件
-
子类对象和父类引用之间有继承关系
-
重写
当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。
要想调用父类中被重写的方法,则必须使用关键字
super
。 -
父类引用指向子类对象:
Parent p = new Child();
即 向上转型
-
-
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
-
浅拷贝:它会在堆中创建一个新对象。新对象和原对象本身没有任何关系,但属性相同
如果属性是基本类型,拷贝的就是基本类型的值;
如果属性是引用类型,拷贝的就是引用地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
-
深拷贝:会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。即 拷贝的新对象完全独立于原对象。
-
引用拷贝:引用拷贝会生成一个新的对象引用地址,但是两个最终指向依然是同一个对象,并没有创建出一个新的对象。因为是同一个对象的引用,所以两者改一个,另一个对象的值也随之改变
Java中的常见对象
-
Object
Object 类是一个特殊的类,是所有类的父类。
-
String,String 属于引用数据类型,是个类。
-
String 为什么是不可变的?
因为
String
类中保存字符串的数组被final
和private
关键字修饰,所以不可变public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; //... }
-
String、StringBuffer、StringBuilder 的区别?
重点: 经常需要改变字符串内容时,使用 StringBuffer 和 StringBuilder ; 多线程使用共享变量时使用StringBuffer
-
String 是 final 修饰的,不可变,每次操作都会产生新的 String对象
-
StringBuffer 和 StringBuilder 都是在原对象上操作
-
StringBuilder
与StringBuffer
都继承自AbstractStringBuilder
类 -
可变性:三者都是用字符数组保存字符串,但
String
保存字符串的数组被final
和private
关键字修饰,而StringBuilder
与StringBuffer
保存字符串的数组没有被修饰。所以String
不可变,而其他两个可变 -
线程安全性:
String
不可变,线程安全。StringBuffer
对方法加了同步锁,线程安全StringBuilder
没有加同步锁,线程不安全 -
性能:
StringBuilder
>StringBuffer
>String
-
使用总结:
数据少:
String
数据多且单线程:
StringBuilder
数据多且多线程:
StringBuffer
-
-
字符串拼接用“+” 还是
StringBuilder
?用
StringBuilder.append()
,因为 “+” 实际上也是通过StringBuilder
调用append()
方法实现的 -
String
的equals()
和Object
的equals()
有何区别?String
中的equals
方法是被重写过的,比较的是 String 字符串的值是否相等。Object
的equals
方法是比较的对象的内存地址。 -
字符串常量池的作用是什么?
字符串常量池 是
JVM
为了提升性能针对字符串(String
类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 System.out.println(aa==bb);// true
-
五.泛型
java泛型?什么是类型擦除?介绍一下常用的通配符?
-
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
-
类型擦除:Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
-
常用的通配符为: T,E,K,V,?
-
? 表示不确定的 Java 类型
-
T (type) 表示具体的一个 Java 类型
-
K V (key value) 分别代表 Java 键值中的 Key Value
-
E (element) 代表 Element
-
六.反射&注解
-
反射:
动态语言:运行时可以改变其结构。
Java
被认为是动态语言的关键:反射机制加载完类后,堆内存中会产生一个
Class
类型的对象(一个类只有一个Class
对象),此对象包含了类的全部信息,像一面镜子,通过此对象看到类的结构的操作:反射具体作用: 反射机制可以动态的获取程序的信息 及动态调用对象。
-
获取反射的 3 种方法:
1.通过new对象实现反射机制
Student student=new Student(); Class aClass = student.getClass(); System.out.println(aClass.getName());
2.通过路径实现反射机制 ,相对的路径,如果是在同一个包下面的话,可以直接写
Class aClass1 = Class.forName("Student"); System.out.println(aClass1.getName());
3.通过类名实现反射机制
Class studentClass = Student.class; System.out.println(studentClass.getName());
-
-
注解:
可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。
注解本质是一个继承了
Annotation
的特殊接口注解只有被解析之后才会生效,常见的解析方法有两种:
-
编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -
运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
-
七.异常
Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
-
Exception
: 程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为 Checked Exception (编译时异常,必须处理) 和 Unchecked Exception (运行时异常,可以不处理)。 -
Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获 。例如Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Checked Exception
和 Unchecked Exception
有什么区别?
-
Checked Exception 即受检查异常 即 编译时异常,Java 代码在编译过程中,如果编译时异常没有被
catch
/throw
处理的话,就没办法通过编译 。 -
Unchecked Exception 即 不受检查异常 即 运行时异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
try-catch-finally 如何使用?
-
try
块: 用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。 -
catch
块: 用于处理 try 捕获到的异常。 -
finally
块: 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。如:
try { System.out.println("Try to do something"); throw new RuntimeException("RuntimeException"); } catch (Exception e) { System.out.println("Catch Exception -> " + e.getMessage()); } finally { System.out.println("Finally"); }
八.IO
什么是序列化?什么是反序列化?
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
-
序列化: 将数据结构或对象转换成二进制字节流的过程
-
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object),也就是实例化后的类(Class)。
Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient
关键字修饰。 transient : adj. 暂时的
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
关于 transient
还有几点注意:
-
transient
只能修饰变量,不能修饰类和方法。 -
transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。 -
static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
获取键盘输入常用的两种方法
-
通过
Scanner
类Scanner input = new Scanner(System.in); String s = input.nextLine(); input.close();
-
通过
BufferedReader
类BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); String s = input.readLine();
Java 中 IO 流分为几种?
-
按照流的流向分,可以分为输入流和输出流;
-
按照操作单元划分,可以划分为字节流和字符流;
-
按照流的角色划分为节点流和处理流。
Java IO 流的类都是从如下 4 个抽象类基类中派生出来的。
-
InputStream / Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
-
OutputStream / Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作对象分类结构图:
既然有了字节流,为什么还要有字符流?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:
字符流是由 Java 虚拟机将字节转换得到的:
-
过程非常耗时,
-
不知道编码类型就很容易出现乱码问题。
所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。
总结:
音频文件、图片等媒体文件用 字节流 比较好
涉及到字符的话使用 字符流 比较好。
九. 其他
1. 乐观锁和悲观锁的区别
-
乐观锁:是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的 那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回 滚。
-
悲观锁:每次在拿数据的时候都会上锁