基本知识
1、JVM、JDK和JRE
JVM:是运行Java字节码的虚拟机,字节码和不同系统的虚拟机是实现Java语言跨平台特性的关键所在。我们常用的是HotSpotVM仅仅是JVM的一种实现而已,只要符合JVM规范,各种机构组织都可以构建自己的JVM。
JDK:它拥有JRE拥有的一切,还有编译器(javac)和工具(javadoc、jdb),能够创建和编译程序。
JRE:是Java行时环境,包括:JVM,Java类库、Java命令;但它不能构建新的程序。
2、字节码文件
字节码:是JVM可以理解的代码,不面向任何特定的处理程序,只面向JVM。字节码装换为机器码,需要通过解释器,JVM类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这样的执行效率较低,而且有很多方法是需要多次被调用执行的,所以就引进了JIT(just-in-time-compilation)编译器。JIT属于运行时编译,当JIT编译器第一次编译后,会将字节码对应的机器码保存下来,下一次可以直接使用。所以说Java是解释和编译共存的语言。
注:HotSpot采用惰性评估的做法,根据二八定律,占用大量系统资源的是小部分热点代码,JVM会根据代码的执行情况,执行次数做出一定优化,执行的次数越多,速度也就会越快;JDK9引入了一种新的编译模式AOT(ahead-of-time-compilation),直接将字节码编译成机器码,省去了JIT预热等占用的资源,但是AOT的编译质量是比不上JIT的。
3、Java是解释和编译共存的语言?
编译型语言通过编译器将源代码一次性翻译成可以直接在平台执行的字节码文件,执行效率高,开发效率低。(C、C++、Go、Rust)
解释型语言通过解释器将源代码一句句解释为机器码后执行。执行效率低,开发效率高。(Java、Python、PHP)
即时编译技术:现将源代码编译成字节码,在执行时,将字节码直译为机器码执行
Java需要javac先编译为字节码,再通过解释器执行解释为机器码,所以Java是解释和编译共存的语言
4、Open JDK和Oracle JDK
- Open JDK是一个参考模型并完全开源;Oracle JDK是Open JDK的具体实现,部分开源。
- Open JDK和Oracle JDK代码基本相同,但Oracle JDK经过了彻底的测试和稳定,更加适用于开发
- Oracle JDK的相应和JVM性能方面更高
- Oracle JDK对即将发布的版本不会提供长期支持,用户需要获取最新版本获取支持。
字符相关:
- 英文对应的字符编码方式是:ASCII码。ASCII码采用1byte进行存储;'a’是97开始 'A’是65开始 '0’是48开始…
- ISO-8859-1编码:又称为latin-1编码方式,向上兼容ASCII码。但不支持中文。
- 简体中文:GB2312<GBK<GB18030 (容量的关系)繁体中文:big5
- java为了支持所有语言,使用了unicode编码,Unicode编码是十六进制的 ,具体实现有:UTF-8 UTF-16…
- 计算机在底层存储数据的时候,一律存储的是“二进制的补码形式”,补码形式效率最高。
基本语法
1、字符型常量和字符串常量的区别?
- 字符型常量是单引号引起的一个字符,相当于一个整型值(ASCII),可以参与表达式运算;占用2个字节(char):‘a’
- 字符串常量是双引号引起的多个字符,是一个地址值,该字符串在内存中的内存地址,占用多个字节:“abc”
2、可变长参数?
- 从Java5开始,Java支持定义可变长参数
- 如果一个方法传入多个类型的参数,可变长参数只可以作为最后一个参数传入,但其前面也可以没有其他参数
public static void testArgs01(String arg1,String... args) {
}
public static void testArgs01(String... args) {
}
- 当遇到方法重载时,会优先匹配固定参数的方法,因为相比于不定长参数而言,固定参数的方法匹配度肯定更高,因为一切都是确定的。
- 可变长参数编译后实际是一个数组
3、注释
- 分为单行注释、多行注释、文档注释
- 编译器在编译代码之前会把代码中所有的注释抹掉,字节码中是不保留注释的
- 代码的注释并不是越多越好,逻辑清晰、语法规范的代码本身就是最好的注释
4、标识符和关键词
-
关键词:是被赋予特殊含义的标识符,都是小写
-
标识符:给类、方法、变量等起的名字就叫做标识符
标识符的命名规则:
- 只能用:数字、大小写字母、下划线、&
- 不能以数字开头,不能含有空格
- 不能含有关键字
- 区分大小写
- 理论上没有长度限制
标识符的命名规范:
- 见名知意
- 驼峰命名
- 类名、接口名:首字母大写,后面每个单词大写
- 变量名、方法名:首字母小写,后面每个单词大写
- 常量名:全部大写,每个单词之间用下划线连接
5、变量
-
在Java中数据被称为字面量,使用变量可以反复利用内存
-
变量其实就是内存当中存储数据的最基本的单元。
-
变量三要素:变量名、数据类型、值
-
变量使用”=“赋值,在运行时,等号右边先执行,再赋值给左边
-
变量可以重复赋值,但不能重复声明,可以同时声明多个变量
- 局部变量:在方法体中声明的变量,仅在本方法中有效
- 成员变量:在类体内声明,同一个类的所有方法都可以使用
-
变量的作用域:大括号之内,Java中访问有个就近原则
6、运算符
- 自增自减运算符:++、–。可以放在变量前也可以放在变量后,放在变量前(b=++a)时,先自增再赋值;放在变量后
(b=a++),先赋值,再自增。
-
短路与&&和逻辑&
- 这两个运算符的运算结果没有区别。只不过‘短路与&&’会发生短路现象(右边表达式不执行)。
- 从效率方面来说,&&比&的效率高一些。逻辑与无论左边结果如何,都会去执行右边,影响效率。
-
使用扩展赋值运算符的时候,永远都不会改变运算结果类型。
-
+ 运算符的作用:求和、字符串拼接。当 + 运算符任意一边是字符串类型,+会进行字符串拼接操作。当一个表达式当中有多个加号的时候
遵循“自左向右”的顺序依次执行。(除非额外添加了小括号,小括号的优先级高)
7、控制语句
-
IF语句:
-
对于if语句来说,在任何情况下只能有1个分支执行;
-
带有else分支的,一定可以保证会有一个分支执行,如果没有else分支,可能一个分支也没有执行;
-
当分支当中java语句只有1条,那么大括号{}可以省略
-
-
switch语句:
- switch语句中break语句不是必须的,default分支也不是必须的。
- 如果分支执行了,没有break,就会发生case穿透现象,一直执行下去;
- 当所有的case都没有匹配成功,那么最后default分支会执行。
- switch语句支持int类型和String类型,JDK8之前只支持int。在JDK8之后,switch语句开始支持字符串String类型。switch语句本质上是只支持int和String,但是byte,short,char也可以使用在switch语句当中,因为byte short char可以进行自动类型转换。
- case合并:值和多个case匹配都可以执行某些语句时,可以将case合并写
switch (i) {
case 1: case 2: case 3:
System.out.println(111);
}
-
while语句和do…while语句:while语句可以一开始就不执行,do…while语句至少执行一次
-
continue、break和return的区别
- continue:跳出当前循环,进入下一次循环
- break:跳出当前循环体,执行循环体下面的语句;
- return:跳出当前方法体,结束方法。如果该方法有指定返回值,则return一个符合指定数据类型的值;如果没有指定返回值,直接return即可。
8、方法
-
一个方法就是一个“功能单元”。方法提高了代码复用性。方法体中的代码都必须遵循自上而下的顺序依次逐行执行;除过main方法是由JVM调用的,其他方法都需要手动调用。
-
返回值是指我们获取到的某个方法体执行后的结果,可以return具体的值,没有指定接收返回值,可以直接return;
-
静态方法为什么不能调用非静态成员?
静态方法是属于类的,会在类加载时候被执行,而非静态成员属于实例对象的,只有在对象被实例化之后,才可以访问。
-
静态方法和实例方法有何不同?
- 调用方式不同,静态方法使用
类名.方法名
或对象.方法名
,当在一个方法中调用本类中的另一个方法时,类名.
可以省略。实例方法只能使用对象.方法名
。注:静态方法不属于某个对象,而属于这个类 - 静态方法在访问本类的成员时,只能访问本类的静态成员,无法访问实例成员。
- 调用方式不同,静态方法使用
-
方法重载和方法重写的区别?
- 方法重载:发生在同一个类当中,或是父子类当中;方法名相同,参数类型、个数、参数顺序不同。方法返回值和访问修饰符可以不同。
- 方法重写:重写是子类对父类的允许访问的方法的实现过程进行重写,发生在运行期。
1. ==”两同“:==方法名、参数列表必须相同;
2. “两小”:子类的返回值类型应该比父类方法更小或相等(当父类返回值是基本数据类型或void,子类的返回值类型就不能变;当父类返回值是引用数据类型,子类的返回值类型可以是这个引用数据类型的子类);抛出的异常范围应该比父类更小或相等;
3. “一大”:访问修饰符的范围应该大于等于父类。
4. 当父类的访问修饰符是private、final、static
,子类就不能重写该方法。但被static
修饰的方法可以被子类再次声明。
5. 构造方法不能被重写
9、==和equals()的区别
- == 对于基本数据类型,比较的是值;对于引用数据类型,比较的是对象的内存地址
- equals()不能比较基本数据类型,只能用来判断两个对象是否相等
- equals()方法存在于object类,如果没有重写equals()方法,则等价于使用==比较
- 一般情况下我们都需要重写equals()方法,自定义比较规则。
- String类中的equals()方法是被重写过的,比较的是对象的值相等。
10、equals()和hashCode()
-
hashCode()的作用是获取哈希码,哈希码的作用是确定该对象在哈希表中的索引位置。
-
hashCode()定义在Object类中,是本地方法,是使用C++实现的,通常是将对象的内存地址转换为整数后返回;
-
为什么定义hashCode()这个方法?
答:以hashSet为例,当我们把对象要加入hashSet中时,会先计算该对象的hash值来确定该对象加入容器的位置。同时也会与其它已经加入容器的对象的hash值做对比。
如果没有相同的hash值,就会认为这个对象没有重复出现;
如果出现了相同的hash值的对象,会再调用equals()方法来检查hash值相同的两个对象是否真的相同。如果还是相同,就会判断为相同对象,不允许其再加入容器之中。如果不同,就会重新散列到其他位置。这样就减少了使用equals()的次数,提高了执行速度。
-
equals()和hashCode()都是用来比较对象是否相等的,JDK为什么同时提供这两种方法?
答:如果仅提供equals()方法;以hashSet为例,插入容器时,需要和已插入对象挨个equals进行比较,这样效率太低。使用hash值可以快速确定索引位置;
如果仅提供hashCode()方法;hash值相等的两个对象不一定真的相等,因为hashCode()使用的hash算法是存在多个对象传回相同哈希值的情况。(不同的对象得到相同的哈希值这样的现象称作哈希碰撞)
hash值相同,对象不一定相等;hash值不相同,对象一定不相等;hash值相同别切equals为true,两个对象相等。
-
重写equals()方法时,为什么必须也要重写hashCode()方法?
答:要判断两个相等的对象必须hash值相同,并且equals()方法返回true。只重写equals()方法,可能会导致相同的两个对象hash值不相等。
总结:equals()方法判断两个对象是相等的,那这两个对象的哈希值也必须相等;两个对象只是hash值相等,因为存在哈希碰撞,他们不一定相等。
基本数据类型
-
数据类型用来声明变量,程序在运行过程中根据不同的数据类型分配不同大小的空间。
-
基本数据类型:
- 整数型:byte(字节型,1字节,127)、short(短整型,2字节,32767)、int(整型,4字节,2147483647)、long(长整型,8字节)
- 浮点型:float(单精度,4字节)、double(双精度,8字节)
- 布尔型:boolean(1字节)
- 字符型:char(2字节,65535)
-
小容量可以直接赋值给大容量,称为自动类型转换;大容量不能直接赋值给小容量,需要使用强制类型转换进行强制转换,存在精度损失。
-
当整数型字面量没有超出byte的取值范围-128~127,那么这个整数型字面量可以直接赋值给byte类型的变量。(写代码方便)
-
注意:如果用在银行方面或者财务方面,java中提供了一种精度更高的类型:java.math.BigDecimal
-
任意一个浮点型都比整数型空间大。 float容量 > long容量
-
浮点型数据默认被当做double来处理 。如果想让这个浮点型字面量被当做float类型来处理,那么请在字面量后面添加F/f。
-
整数型的数据默认被当做int类型处理。如果希望该‘整数型字面量’被当做long类型来处理,需要在‘字面量’后面加L/l。
-
多种数据类型混合运算,各自先转换成容量最大的那一种再做运算;
基本数据类型对应的包装类
- 8种基本类型对应的包装类分别为Byte、Short、Integer、Long、Float、Double、Character、Boolean
- 包装类型如果不赋值就是
NULL
,基本类型有其默认值不为NULL
- 基本数据类型直接存放在Java虚拟机栈中的局部变量表中,而包装类属于对象,是存放在堆中的。
- 局部变量表:局部变量表主要存放编译器可知的基本数据类型和对象应用(reference类型,引用指针)
包装类的常量池技术
- Byte、Short、Integer、Long这四种包装类都默认创建了数值在【-128~127】的相应类型的缓存数据。
- Character默认创建了数值在【0~128】的缓存数据
- Boolean会直接返回【true和false】。
- 浮点型的包装类并没有实现常量池技术。
- 所有整型包装类之间的比较,都是使用equals()方法。在缓存池缓存范围内的数据可以直接使用==比较。
自动装箱与自动拆箱
- 装箱:将基本数据类型用它们对应的引用数据类型包装起来
- 拆箱:将包装类型转换为基本数据类型。
- 自动装箱实际上是调用了包装类的valueOf方法,自动拆箱调用了xxxValue()方法
Integer i =1;//自动装箱,1是int类型,直接赋值给Integer类型,基本类型装箱为包装类
Integer integer = Integer.valueOf(1);
System.out.println(integer.equals(i));//true
int n = i;//自动拆箱,i是包装类,直接赋值给int类型,包装类拆箱为基本类型
int nn = i.intValue();
System.out.println(n==nn);//true
注:频繁的拆装箱,会严重影响系统性能。应该尽量避免不必要的拆装箱。
面向对象基础
面向过程比面向对象性能高?
- 面向过程的性能更高;
- 因为面向对象的语言中,类的调用需要实例化,比较消耗资源。当性能是最主要的考量因素的时候,建议使用C、C++等
- 但Java语言的性能不高主要是因为Java是半编译型语言,无法将源码直接编译为CPU执行的二进制码。
成员变量和局部变量的区别有哪些?
-
语法形式:成员变量是属于类的,而局部变量是属于方法或某个代码块的;成员变量是可以被
访问修饰符
和static
修饰的,局部变量不能;但都可以被final
修饰 -
**存储方式:**如果成员变量是用
static
修饰的,就会在类加载时开辟存储空间。而如果没有static
修饰,那就是存储在栈内存中。 -
**生存时间:**成员变量是伴随着对象的创建和销毁,局部变量是伴随方法的调用创建和销毁。
-
**默认值:**成员变量如果没有被赋初始值,会根据类型赋默认值;局部变量则不会默认赋值。
构造方法
-
**构造方法的作用:**完成类对象的初始化工作
-
**构造方法的特点:**构造方法名和类名相同、没有返回值但也无需用
void
声明、创建对象时自动调用、构造方法可以被重载,但不能重写。 -
如果一个类没有声明构造方法,该程序能正确执行吗?
答:如果一个类没有声明构造方法,程序也可以执行。因为即使没有声明构造方法,但也会有默认的无参构造方法。如果我们自己手动添加了构造方法(无论有参还是无参),Java就不会再为我们提供默认的无参构造。此时创建对象就必须要传入相应属性,否则就会报错。所以建议重载了有参的构造方法,就手动的将无参构造也写出来。
面向对象的三大特征
-
**封装:**指将一个对象的属性封装在这个对象的内部,不允许外部直接访问对象的内部属性。但是可以提供一些方法来让外部访问和操作这些属性。
-
**继承:**不同的对象,会有一些共同的点。此时可以把共同的点提取出来封装成一个父类。这样有助于提高代码的复用性。
继承的特点:
- 子类拥有父类所有的属性和方法,但私有的属性和方法只是拥有,却无法访问。
- 子类可以对父类进行扩展,拥有子类特有的属性和方法
- 子类可以用自己的方式实现父类的方法
-
**多态:**表示一个对象具有多种状态,具体表现为父类的引用指向子类的对象。
多态的特点:
- 对象类型和引用类型之间必须具有继承或实现的关系;
- 引用类型的方法执行时到底调用的是哪个类的方法,在运行时才能确定;
- 多态不能调用“只在子类存在,在父类不存在的方法”
- 子类重写了父类的方法,执行的就是子类重写的方法;如果没有重写父类的方法,执行的就还是父类的方法
深拷贝和浅拷贝的区别,什么是引用拷贝?
-
**浅拷贝:**浅拷贝会在堆中新建一个对象,如果原对象是引用类型的话,将原对象的引用地址复制到新对象中。所以浅拷贝对象和原对象共用一个内部对象
-
**深拷贝:**深拷贝会完全复制整个对象,包括所有属性和内部对象。
-
**应用拷贝:**就是两个不同的引用指向同一个对象
常见对象
Object
public final native Class<?> getClass();//获取当前对象执行的Class对象
public native int hashCode();//通过哈希算法获取哈希值,可以看做是对象的内存地址
public boolean equals(Object obj)//比较两个对象相等
protected native Object clone() throws CloneNotSupportedException;//拷贝一份当前对象并返回
public String toString() {}//将对象转换为String类型
public final native void notify();//唤醒当前正在等待的一个线程
public final native void notifyAll();//唤醒当前正在等待的所有线程
public final void wait() throws InterruptedException {}//暂停线程的执行,wait方法是释放锁的
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {//暂停线程的执行并一直执行
protected void finalize() throws Throwable { }//垃圾回收时执行
String、StringBuffer和StringBuilder
- 不可变性
- **
String
类不可变。**因为String类是被final
修饰的;String中包括保存字符串的byte数组在内的所有属性都是被private
和final
修饰的(在JDK9之前,使用char[]数组存储,之后采用byte[]数组存储);并且String类没有提供给外部修改这个字符串的方法 - StringBuffer、StringBuilder可变。
StringBuffer
和StringBuilde
r都继承自AbstractStringBuilder
,AbstractStringBuilde
中也是使用char数组存储,但没有使用final
和private
修饰,并且对外提供了修改字符串的方法。
- **
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
-
线程安全性
String
类不可变,所以是线程安全的。StringBuffer
对父类定义的方法加了同步锁,所以是线程安全的;StringBuilder
对方法并没有添加同步锁,所以是线程不安全的;
-
性能比较
String
每次改变都会新建一个String
对象,性能较低StringBuilder
和StringBuffer
都是对对象本身进行操作。相同情况下,使用StringBuilder
性能仅能提高15%,但却线程不安全。
总结:操作少量数据,使用String
;单线程操作大量数据使用StringBuilder
;多线程操作大量数据使用StringBuffer
。
字符串拼接使用“+”还是StringBuilder的append()方法
Java不支持运算符重载,但“+”和“+=”专门为String
重载过
- 对象引用和“+”的字符串拼接方式,实际上是通过
StringBuilder
调用append()
方法实现的,拼接完成后,调用toString()
方法转化为String
对象 - 当在循环体内使用“+”进行字符串拼接时,每次循环都会创建一个新的
StringBuilder
对象,无法复用
字符串常量池
-
字符串常量池是JVM为了提升性能和减少内存消耗对String专门开辟的一块区域,为了避免字符串的重复创建。
-
在JDK1.7之前,字符串常量池在方法区内;JDK1.7之后,字符串常量池在堆中。
泛型
- **Java泛型(generics)**是JDK5中引入的新特性,泛型提供了编译时类型安全检测机制。这个机制可以帮助程序员在编译时检测非法类型。
- **类型擦除:**Java的泛型是伪泛型,Java在运行期间,所有的泛型信息都会被擦除。
- JDK8.0之后推出的新特性:自动类型推断机制。(又称为钻石表达式)
List list = new ArrayList<>();
-
自定义泛型:泛型类、泛型接口、泛型方法
-
常用的泛型通配符:
- ?:表示不确定的Java类型
- T:表示一个具体的Java类型
- K V:代表键值对中的key和value
- E:代表Element
-
泛型的实际应用
-
定义一个统一的结果返回类来进行数据传输和前端交互
public class Result<T> {
-
反射
- 获取Class对象的四种方式
- 使用类的class属性获取:Class xxx= XXX.class
- Class.forName获取:Class xxx= Class.forName(完整类名)
- 实例化对象通过getClass:XXX xxx = new XXX() Class yyy= xxx.getClass()
- 使用类加载器:Class xxx= ClassLoader.loadClass(完整类名)
- 如果只希望一个类的静态代码块执行,其他方法不执行,可以使用
Class.forName("完整类名");
.这个方法会导致类加载,类加载时静态代码块就会执行。 - ==双亲委派机制:==类加载器收到类加载请求时,并不会自己执行加载,会一层一层向上委托。顶层的类加载器先进行加载。启动类加载器–>扩展类加载器—>应用类加载器。如果父类的加载器无法完成加载任务,子类才会自己加载。
- 反射机制的优缺点:
- 优点:代码更加灵活,java代码写一遍,在不改变java源代码的基础之上,可以做到不同对象的实例化。
- 缺点:安全问题
异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnsC6Obk-1645515095263)(C:\Users\sgxcu\AppData\Roaming\Typora\typora-user-images\image-20220221184216303.png)]
-
Throwable
的两个子类Exception
和Error
Exception
是程序本是可以处理的,Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。- Checked Exception:受检异常需要进行
try/catch
处理,否则会报错 - Unchecked Exception:
RuntimeException
及其子类都统称为非受检查异常
- Checked Exception:受检异常需要进行
Error
:Error
属于程序无法处理的错误 ,例如Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
-
Throwable
的常用方法public String getMessage() {}//异常的简要描述 public String toString() {}//异常发生的详细信息 public void printStackTrace() {}//打印Throwable封装的异常信息 public String getLocalizedMessage() {}//如果子类没有覆盖此方法,会返回和getMessage同样的信息
-
finally
块:finally
块中的语句大多数情况下一定会执行的!当try/catch
语句块中有return
时,finally
块中的语句会在方法返回前被执行。-
finally
块代码不会被执行的情况在执行
finally
被执行之前,如果JVM停止工作,finally
不执行。程序所在的线程死亡,
finally
不执行;关闭CPU,
finally
不执行。 -
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句不会被执行。
-
I/O
序列化和反序列化
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
总结:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
关于 transient
关键字
-
对于不想进行序列化的变量,使用
transient
关键字修饰。 -
transient
关键字的作用:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient
修饰的变量值不会被持久化和恢复。 -
注意:
-
transient
只能 修饰变量,不能修饰类和方法。 -
transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。 -
static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
-
Java中I/O流的分类
- 按照流的方向分类(以内存为参照)
- 输入流:到内存中去,叫做输入(Input)。或者叫做读(Read)。
- 输出流:从内存中出来,叫做输出(Output)。或者叫做写(Write)。
- 按照读取数据方式不同进行分类
- 字节流:按照字节的方式读取数据,一次读取1个字节。这种流是万能的,什么类型的文件都可以读取。
- 字符流:按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的。
I/O流的四大抽象父类
-
InputStream (字节输入流)、OutputStream (字节输出流)、Reader(字符输入流)、Writer(字符输出流)
Stream
结尾的都是字节流。以“Reader/Writer”
结尾的都是字符流。
注:所有的流在使用结束后,都要使用close()方法关闭;所有的输出流在最终输出时候都要使用flush()方法刷新流。
-
文件专属:FileInputStream、FileOutputStream、FileReader、FileWriter
-
转换流:(将字节流转换成字符流)InputStreamReader、OutputStreamWriter
-
缓冲流专属:BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream
-
数据流专属:DataInputStream、DataOutputStream
-
标准输出流:PrintWriter、PrintStream
-
对象专属流:ObjectInputStream、ObjectOutputStream
既然有了字节流,为什么还要有字符流?
- 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节
- 字符流是由JVM将字节流转换得到的,这个过程还比较耗时。并且当我们不知道接收的数据的编码类型,就会产生乱码问题。所以I/O提供了一个字符流方便我们操作文本的读写。其他类型的文件还是需要使用字节流读写。
I/O模型
- 从计算机结构的视角来看, I/O 描述了计算机系统与外部设备之间通信的过程。
- 从应用程序的角度来看,为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space )。
- 我们的应用程序实际上只是发起了 IO 操作的调用而已,具体IO 的执行是由操作系统的内核来完成的
- BIO(同步阻塞IO模型):用户发起read调用后,进程会一直阻塞,直到内核将数据拷贝到应用空间。
- NIO(同步非阻塞模型):用户会一直发起read调用轮询访问,避免了进程一直阻塞的情况,直到内核将数据拷贝到应用空间。==(但轮询访问数据是否准备完成很耗费资源)==Java中的NIO是从Java1.4引进的。适用于高并发、高负载的网络应用。
- I/O多路复用模型:线程首先发起
select
调用,询问内核空间,数据是否准备完毕,确定准备完毕,用户线程才会发起read调用。减少了无效的系统调用。
代理模式
- 使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
- 代理模式的主要作用是扩展目标对象的功能
静态代理
静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。
-
**从 JVM 层面来说, **静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
-
静态代理实现步骤:
-
定义一个接口及其实现类;
public interface MessageService { String send(String message); }
public class MessageServiceImpl implements MessageService{ @Override public String send(String message) { System.out.println("发送信息:"+message); return message; } }
-
创建一个代理类同样实现这个接口
public class MessageProxy implements MessageService{ private MessageService messageService; public MessageProxy(MessageService messageService) { this.messageService = messageService; } @Override public String send(String message) { System.out.println("调用方法前执行"); messageService.send(message); System.out.println("最后执行的"); return null; } }
-
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
public class ProxyTest { public static void main(String[] args) { MessageService messageService = new MessageServiceImpl(); MessageProxy proxy = new MessageProxy(messageService); proxy.send("Hello Proxy!"); } }
-
动态代理
动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
-
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
-
Java动态代理机制的核心:
InvocationHandler
接口和Proxy
类是核心。 -
JDK动态代理使用步骤:
-
定义一个接口及其实现类;
public interface MessageService { String send(String message); }
public class MessageServiceImpl implements MessageService{ @Override public String send(String message) { System.out.println("发送信息:"+message); return message; } }
-
自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;* 动态代理 */ public class DynamicInvocationHandler implements InvocationHandler { //代理对象 private Object proxy; public DynamicInvocationHandler(Object proxy) { this.proxy = proxy; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("调用方法前执行"); Object result = method.invoke(proxy, args); System.out.println("最后执行的"); return result; } }
-
通过
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象;
public class JdkProxyFactory { public static Object getProxy(Object proxy) { return Proxy.newProxyInstance( proxy.getClass().getClassLoader(), // 目标类的类加载 proxy.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个 new DynamicInvocationHandler(proxy) // 代理对象对应的自定义 InvocationHandler ); } }
-
测试
MessageService messageService1 =(MessageService) JdkProxyFactory.getProxy(new MessageServiceImpl()); messageService1.send("Hello Spring...");
-
静态代理和动态代理的对比
-
灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
-
JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
Java 中只有值传递
-
值传递 :方法接收的是实参值的拷贝,会创建副本。
-
引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
-
Java 中将实参传递给方法(或函数)的方式是 值传递 :
-
如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
-
如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
-
使用BigDecimal解决浮点数运算精度损失的问题
为什么float
或double
运算存在精度损失的问题?
-
因为计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
-
计算机在存储小数时候,不断乘以2,当不再存在小数位时候,得到的整数部分就是二进制的结果
BigDecimal
-
需要对浮点数进行精确运算的业务中,都要使用
BigDecimal
-
浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
解决方法就是使用
BigDecimal
定义浮点数,在进行运算
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));//1.2
System.out.println(a.subtract(b));//0.8
System.out.println(a.multiply(b));//0.20
System.out.println(a.divide(b));//5
- 注意事项:我们在使用
BigDecimal
时,为了防止精度丢失,推荐使用它的BigDecimal(String val)
构造方法或者BigDecimal.valueOf(double val)
静态方法来创建对象。