目录
1. 语法
1.1 final关键字的使用
在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)
- 修饰类
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。 - 修饰方法
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。 - 修饰变量
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
public static void main (String[] args) throws java.lang.Exception
{
final int i;
i=9999; //第一次初始化是可以的
i=777; //第二次复制就不行了 error: variable i might already have been assigned
System.out.println(i);
}
1.2字符串相关
1.2.1 字符串在内存中是怎么存储的
https://blog.csdn.net/okyoung188/article/details/55506594
在java中,内存分成两个区域stack 和 heap , stack 用于运行(包括变量引用和逻辑运行),heap 用于存储变量实体。java中对String对象特殊对待,所以在heap区域分成了两块,一块是String constant pool,用于存储java字符串常量对象,另一块用于存储普通对象及字符串对象。
而string的创建有两种方法:String a = “abc”; String b=new String(“abc”);
对于第一种,jvm会首先在String constant pool 中寻找是否已经存在"abc"常量,如果没有则创建该常量,并且将此常量的引用返回给String a;如果已有"abc" 常量,则直接返回String constant pool 中“abc” 的引用给String a.此创建方法之会在String constant pool中创建对象。
对于第二种,jvm会直接在非String constant pool 中创建字符串对象,并不会把"abc” 加入到String constant pool中,并把该对象 引用返回给String b;
虽然new String()方法并不会把"abc” 加入到String constant pool中,但是可以手动调用String.intern(),将new 出来的字符串对象加入到String constant pool中。
1.3 基本数据类型
1.3.1 Java有8种基本类型
数字类型占用空间从小到大有:byte(1),short(2),int(4), long(8),float(4),double(8),括号里的是字节
字符类型有:char(2)
布尔类型:boolean,依赖于JVM厂商的具体实现。
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c =3;
Integer d=3;
//通过这种Integer d = N的方法创建对象,当这个值在[-128,127]之间,便返回缓存中已经存在的对象的引用,否则创建一个对象。
//Byte,short,Integer,Long的范围是[-128,127].
//Character是[0,127],Boolean直接返回True和False,但是浮点数没有使用这种常量池技术
Integer e=321;
Integer f =321;
Long g = 3L;
Long h = 2L;
//当等号两边的操作数都是包装器类型的引用,比较的是是否为同一个对象,如果其中有一个操作数是表达式,,比较的就是数值了。
System.out.print("1:");System.out.println(c==d);//true
System.out.print("2:");System.out.println(e==f);//false
System.out.print("3:");System.out.println(c==(a+b));//true
//a先拆箱,b也拆箱,然后相加,然后结果再装箱
System.out.print("4:");System.out.println(c.equals(a+b));//true
System.out.print("5:");System.out.println(g==(a+b));//true
//a+b包装类为Integer,g是Long,包装类不同,结果为false
System.out.print("6:");System.out.println(g.equals(a+b));//false
//a+h包装类上会晋升为Long,然后Long和Long的equals就能正常使用。
System.out.print("7:");System.out.println(g.equals(a+h));
System.out.print("8:");System.out.println(g.equals(c));
}
1.4 方法
1.4.1 Java中只有值传递
按值调用表示方法接收的是调用者提供的值,按引用调用表示方法接收的是调用者提供的变量地址。
1.4.2 方法签名
要完整地描述一个方法,需要指出方法名和参数类型,这叫做方法的签名。比如String类有4个indexOf的公有方法,它们的签名是:
indexOf(int)
indexOf(int,int)
indexOf(String)
indexOf(String,int)
返回类型不是方法签名的一部分,也就是说,不能有两个名字相同,参数类型也相同却返回不同类型值的方法。
class test
{
public static void main (String[] args) throws java.lang.Exception
{
int i=0;
System.out.println(i);
}
public int get(int a,int b){return 0;}
public void get(int a,int b){} //报错 error: method get(int,int) is already defined in class test
}
1.4.3 重载和重写的区别(两者都是多态的表现)
- 如果多个方法有相同的名字,不同的参数,便产生了重载。如果两个函数已经有重载的关系,那么他们的返回类型可以不同,是否重载只看名字和参数。是编译时多态。
- 重写就是当子类继承自父类的相同方法,输入数据一样,但是要做出有别于父类的响应时,就需要覆盖父类方法。是运行时多态。
2.1 返回值,名字和参数列表必须相同,抛出异常的范围小于等于父类,访问修饰符的范围大于等于父类。
2.2 构造方法无法被重写。
2.3 如果父类方法访问修饰符是private/final/static,则子类不能重写该方法,但是被static修饰的方法可以被再次声明。子类可以继承父类的静态方法,但是不能对这个静态方法进行重写,只能自己再声明(创造属于子类的)一个同名的静态方法。例子如下:
class parent{
public static void printA() {
System.out.println("父类静态方法");
}
public void printB() {
System.out.println("父类普通方法");
}
}
class child extends parent{
public static void printA() {
System.out.println("子类静态方法");
}
public void printB() {
System.out.println("子类普通方法");
}
}
调用子类的方法,这是调用子类自身的静态方法
public class Main {
public static void main(String[] args){
child c1 = new child();
c1.printA();
c1.printB();
}
}
// 输出
子类静态方法
子类普通方法
向上转型的时候,对象调用的方法要么是父类未被覆盖的方法,要么是父类被覆盖的方法。这是输出了父类静态函数应该输出的内容,说明父类的静态方法没有没覆盖重写。
public class Main {
public static void main(String[] args){
parent c2 = new child();
c2.printA();
c2.printB();
}
}
// 输出
父类静态方法
子类普通方法
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/basis/Java%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可以修改 | 子类的返回值类型应该比父类的返回值类型更小或相等 |
异常 | 可以修改 | 子类抛出的异常应该比父类抛出的类型更小或相等 |
访问修饰符 | 可以修改 | 子类的访问修饰符不能比父类的更严格 |
发生阶段 | 编译期 | 运行期 |
重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。
深拷贝vs浅拷贝
- 浅拷贝:对基本数据类型和引用数据类型都进行简单的拷贝,原对象和复制出来的新对象的引用数据类型指向的是同一个对象。
- 深拷贝:对基本的数据类型进行简单的拷贝,对于引用数据类型,创建一个新的对象,并把该引用数据类型引用的对象的内容复制到新的对象。
2. Java面向对象
- 什么时候用接口,什么时候用虚拟类
抽象类是is-a的关系,接口是has-a;接口一般都是添加额外的功能。
抽象类和它的子类之间应该是一般和特殊的关系,而接口仅仅是它的子类应该实现的一组规则
2.1 类和对象
2.1.1 面向对象和面向过程的区别
- 面向过程:面向过程性能比面向对象高。因为类调用的时候需要实例化,开销大,所以单片机和嵌入式开发都面向过程开发。但是面向过程跟面向对象相比,难于维护,不易复用,不易扩展。
- 面向对象:因为面向对象有封装,继承和多态的特征,所以面向对象易维护,易复用,易扩展。但是性能较低。
2.1.2 构造器
当类没有提供任何构造器的时候,系统会提供一个默认的(无参的)构造器。当我们自己添加了类的构造方法(无论是否有参),Java就不会再添加默认的无参构造函数了。所以如果我们提供一个有参的构造器,但是没有提供无参的构造器,在构造对象时如果没有提供参数的话,就会报错。
2.1.2 子类构造器
public Manager(String name,double salary,int year,int month,int day ){
super(name,salary,year,month,day);
bonus=0;
}
这里Manager是Employee类的子类,其中salary是Employee类的私有域,Manager不能直接访问初始化,需要调用父类的构造函数super()来初始化。使用super调用构造器的语句必须是子类构造器的第一条语句。
**如果子类的构造器没有显式调用超类的构造器,那么自动调用超类的默认(没有参数)的构造器。**如果超类没有不带参数的构造器,并且子类的构造器中没有显式调用超类构造器,那么Java编译器将报告错误。
另外,this调用本类的其他构造方法时,也要放在首行。
2.2 面向对象的三大特征
2.2.1 封装
封装是把一个对象的状态信息(也就是属性)隐藏在对象的内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
2.2.2 继承
继承是一种技术,使用已存在的类的定义作为基础建立新的类。新类可以增加新的功能和数据,也可以使用父类的功能。通过使用继承可以快速创建新的类,提高代码的重用率,提高可维护性,节省创建类的时间,提高开发效率。
继承需要记住2点
- 子类拥有父类对象的所有属性和方法(包括私有属性和私有方法),但是私有属性和私有方法是无法访问的,只是拥有而已。
2.2.2 多态
- 有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则,它表明子类的每个对象也是父类的对象。
- “is-a”规则的另一种表述是置换法则,表示程序中出现父类对象的任何地方都能用一个子类对象置换。例如可以将一个子类对象赋值给父类引用。
- 引用类型变量的方法调用的是哪个类的方法,必须在程序运行期间才能确定。
2.2.1 多态存在的三个前提条件
- 继承
- 重写
- 父类引用指向子类对象
2.2.2 多态的两种表现形式
- 重载
- 多态
2.3 修饰符
2.3.1 static
2.3.1.1 在静态方法里不能调用非静态成员
- this和super不能用于static方法中,因为this指向本类对象,super指向父类的对象,所以this和super都是属于对象范畴的东西,而静态方法是属于类范畴的东西。
2.3.1.2 静态变量存放在Java内存区域的方法区
方法区用来存储被虚拟机加载后的类信息,常量,静态变量,即时编译器编译后的代码。HotSpot虚拟机团队用永久代来实现方法区,因此HotSpot虚拟机中方法区也被称为永久代。
2.3.2 静态代码块,代码块和构造函数的执行顺序
public class solution {
public static void main(String[] args) {
// Animal animal = new Animal();
System.out.println("---------------------------------------");
Dog dog = new Dog();
}
}
class Animal{
{
System.out.println("动物的代码块");
}
static {
System.out.println("动物里的静态代码块");
}
Animal(){
System.out.println("动物的构造函数");
}
}
class Dog extends Animal{
{
System.out.println("狗里的代码块");
}
static {
System.out.println("狗里的静态代码块");
}
Dog(){
System.out.println("狗的构造函数");
}
}
输出结果是
---------------------------------------
动物里的静态代码块
狗里的静态代码块
动物的代码块
动物的构造函数
狗里的代码块
狗的构造函数
当把solution这个类main方法第一行的注释去掉,结果为
动物里的静态代码块
动物的代码块
动物的构造函数
---------------------------------------
狗里的静态代码块
动物的代码块
动物的构造函数
狗里的代码块
狗的构造函数
所以执行的顺序是,静态代码块(优先级最高,在类加载的时候就调用了,父类的静态代码块先执行,并且一个类的静态代码块只执行一次),父类的代码块和构造函数,子类的代码块和构造函数。代码块不像静态代码块,代码块可以多次执行,每一次创建对象都会调用。
一个类中的静态代码块可以有多个,位置可以随便放,它不在任何方法体内。JVM加载类的时候会执行它们。如果静态代码块有多个,JVM回按照它们在类中出现的先后顺序依次执行它们。每一个代码块只会被执行一次。普通代码块在这方面跟静态代码块类似。
package ceshi.代码块;
public class many_static_block {
public static void main(String[] args) {
Animal1 a = new Animal1();
}
}
class Animal1{
static {
System.out.println("静态代码块1");
}
{
System.out.println("代码块1");
}
Animal1(){
System.out.println("构造函数");
}
{
System.out.println("代码块2");
}
static {
System.out.println("静态代码块2");
}
}
结果如下:
静态代码块1
静态代码块2
代码块1
代码块2
构造函数
2.3.2.1 注意事项
- 静态代码块对于定义在它后面的静态变量,可以赋值,但是不能访问。
- 静态代码块可能在第一次new的时候执行,也可在Class.forName(“ClassDemo”) 创建Class对象的时候也会执行。
2.4 抽象类和接口的区别
没有普通的成员变量,只有常量,默认使用public static final修饰;
可以有普通的成员变量,并且可以用private修饰
接口没有静态代码块
抽象类可以有静态代码块
接口没有构造函数
抽象类有构造函数
接口是多继承的
抽象类是单继承的
从设计层面来说,抽象类是对类的抽象,是一种模板设计。接口是对行为的抽象,是一种行为的规范。
2.4.1 实现接口的类需要实现接口内的所有方法吗?
抽象类实现某个接口,可以不实现所有接口的方法,可以由它的子类实现。而普通类即非抽象类则必须实现接口里的全部方法,实现的时候可以是空实现(方法体为空没有任何作用)
2.4.2 接口的方法
- 接口有4种方法。抽象方法,默认方法,静态方法和私有方法。其中默认方法,静态方法和私有方法都可以在接口中有实现。
2.5 其他重要的知识点
2.5.1 String,StringBuilder和StringBuffer的区别是什么?为什么String是不可变的
- String类使用final关键字修饰char数组来保存字符串,所以String对象是不可变的。在Java9之后,String类的实现改用byte数组来存储private final byte[] value;
- StringBulider和StringBuffer继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组来保存字符串,但是没有用final关键字修饰,所以这两种对象都是可变的。
- StringBuffer对方法加了同步锁,所以是线程安全的。StringBuilder没有对方法加同步锁,所以是非线程安全的。
2.5.2 Object类的常见方法总结
public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作
2.5.3 ==和equals的区别
“==” 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型,比较的是值,引用数据类型比较的是内存地址)。
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
2.5.4 hashCode()和equals()
hashCode()介绍
hashCode()定义在JDK的Object类中,这就意味着Java的任何类都有hashCode()方法。Object的hashCode方法是本地方法,也就是用C语言或C++来实现,该方法通常用于将对象的内存地址转换为整数后返回。hashCode()在确定该对象在hash表的哪一个位置会用到以及判断元素是否重复的时候会用到。比如在ConcurrentHashMap1.8中,直接用hashCode()与n-1作异或(n是数组长度)得到对象在hash表的哪一个位置。在HashMap1.8中,首先得到元素的hashcode,然后把hashcode右移16位跟原来的hashcode逐位异或,最后再跟n-1逐位与操作,最后得到对象在hash表的哪一个位置。
2.5.4.1 为什么要有hashcode
以插入hashmap为例说明为什么要有hashcode。
在把目标元素插入HashMap的时候,先根据hashcode计算出插入位置,然后判断插入的位置是否为空,如果不为空的话,那就遍历该位置的链表中的元素,判断这些元素的hashcode跟目标元素的hashcode是否相同。如果发现hashcode相同,那就调用equals检查hashcode相等的对象是否真的相同,如果相同,那么插入失败,如果不同,那就插入到链表或红黑树中。
2.5.4.2 为什么重写equals时还要重写hashCode方法?
- Java编程规定了如果两个对象根据equals方法比较是相等的,那么调用这两个对象的hashcode方法都会产生同样的结果。定义这条规则是为了提高hash表的效率,在hash表中,如果两个元素的hashcode不同,说明这两个元素不同,那就没有必要进行equals的比较了,这就大大减少了equals的比较次数。如果没有重写hashcode的话,那么相同的对象可能返回不同的hashcode,这就违反了这一条规则。
2.5.5 Java序列化中如果有字段不想进行序列化,怎么办?
对于不想进行序列化的变量,用transient来修饰。transient只能修饰变量,不能修饰类和方法。
2.5.4 Java和C++的区别
- Java程序编译后的代码是class文件,不能直接被硬件系统直接运行。不同的硬件平台装有不同的JVM虚拟机,由JVM来把class文件翻译成对应硬件平台能够识别的代码。
C/C++编译后的代码能够直接在硬件平台上运行,但是编译后的代码换一个平台就不能执行了。
所以,Java是编译文件级的跨平台,c/c++是源代码级的跨平台 - C++的类可多继承,Java只能单继承
- Java运行在虚拟机上,不需要考虑内存管理和垃圾回收机制。但是C++需要考虑。
3. Java核心技术
3.1 反射机制
Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的所有方法和获取其属性的值。这种动态获取信息和动态调用对象的方法的功能成为java语言的反射机制。
3.1.1 获取Class文件的四种方法
-
class clazz=class.forName(" 权限类名");
-
class clazz=类名.class;
-
class clazz=对象.getclass();
-
通过类加载器获得
3.1.2 反射机制的优缺点
- 优点:运行期判断类型,动态加载类,可以提高代码的灵活度、
- 缺点:1. 性能瓶颈:反射相当于一系列解释操作,性能比直接的Java代码慢很多。2. 安全问题,因为可以访问私有的属性和方法,增加了类的安全隐患。
3.1.3 反射的主要用途
- 用于IDE,当我们输入一个对象或类并想调用它的属性和方法时,IDE就会自动列出它的属性和方法。
- 很多框架(如spring)都是配置化的,为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这时候用到反射,运行时动态加载对象。
3.1.4 为什么反射比较慢
- 因为反射是在运行时而不是在编译时,所以不会利用到编译优化。同时因为是动态生成,因此反射比较慢。(待验证)
3.2 异常
3.2.1 Java异常类的层次结构
Error是描述了Java运行时系统内部错误和资源耗尽错误,它是程序无法处理的错误。如OutOFMemoryError和StackOverFlowError等,这些错误在应用程序的控制和处理能力之外,对于设计合理的应用程序来说,即使发生了错误,本质上也不应该试图去处理它所引起的异常情况。
Exception是程序本身可以处理的异常。分成两类,一类是RuntimeException,另一类是其他异常。“如果出现RuntimeException,那么一定是你的问题”是一条相当有道理的规则。
注意:异常和错误的区别:异常能够被程序处理,错误是无法处理的。
派生于Error类或者RuntimeException的所有异常都是非受查(unchecked)异常,所有其他异常称为受查(checked)异常,一个方法必须声明所有可能抛出的受查异常,否则,比一起就会发生错误。
如果子类覆盖了父类的一个方法,子类的方法声明的受查异常不能比超类声明的方法更加通用(也就是说,子类的异常可以抛出更特定的异常,或者不抛出任何异常)
3.2.2 Throwable类常用的方法
- public stirng getMessage(): 返回异常发生时的简要描述
- public string toString(): 返回异常发生时的详细信息
- public void printStackTrace(): 在控制台打印Throwable对象封装的异常信息。
3.2.3 try-catch-finally
- **try块:**用于捕获异常,后面可接0个或多个catch块,如果没有catch块,则必须跟一个finally块。
- **catch块:**用于处理try捕获到的异常
- **finally块:**无论是否捕获异常,finally块的异常都会被执行。当try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行,并且finally语句的返回值会覆盖原始的返回值。如果已经有try-catch,那么finally可以没有。
public class Test {
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
}
如果调用 f(2),返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。
在以下4种特殊情况下,finally不会被执行
- finally语句块的第一行发生了错误
- 在异常语句前使用了System.exit(int),这时候已经退出了程序,finally不会执行。如果System.exit(int)在异常语句之后,那么finally会执行。
- 程序所在线程死亡
- CPU被关闭。
3.2.4 使用try-with-resources来代替try-catch-finally
- 使用范围是:任何实现了AutoCloseable接口或者Closeable接口的对象
- 关闭资源和finally的执行顺序:在try-with-resources语句中,catch块和finally块在声明的资源关闭后再运行。
《Effective Java》指出:
面对必须关闭的资源,应该优先使用try-with-resources而不是try-finally。随之产生的代码更简短,更清晰,产生的异常更有用。用try-catch-finally可能导致很多问题。
3.3 多线程
- yield方法
放弃当前的CPU资源,让给其他任务去执行。当时放弃的时间不确定,有可能刚刚放弃,又马上获得。 - join方法
在B线程中调用了线程A的join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join() //调用join方法,等待线程t执行完毕
t.join(1000) //调用join方法,等待时间是1000毫秒
3.4 文件与I\O流
3.4.1 Java中IO流分为几种
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流,Java中的字符是Unicode编码,一个字符占用两个字节。
- 按照流的角色可以分为节点流和处理流。节点流可以从一个特定的数据源读写数据(如文件,内存),处理流是“链接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更强调的读写功能。
Java IO的40多个类都是从4个抽象基类派生出来的 - InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。
使用了装饰器模式和适配器模式
3.4.1.1 既然有了字节流为什么还要有字符流
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在①这个过程还算是非常耗时,并且,②如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
3.4.1.2 BIO,NIO和AIO的区别
https://blog.csdn.net/ruanjianxueyuan113/article/details/108996782
4.一些坑
4.1 Collection.toArray()的正确用法
该方法是一个泛型方法: T[] toArray(T[] a); toArray方法中需要传递参数,如果没有传递参数的话,那么返回的就是Object类型的数组。
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//没有指定类型的话会报错
4.2 不要在foreach循环里进行元素的remove/add操作
https://blog.csdn.net/qq_36827957/article/details/88415168
Java语言从JDK1.5.0开始引入foreach循环,通常称之为增强for循环。
4.2.1 问题
当使用增强for循环的时候进行删除
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
}
}
以上的代码尝试删除Hollis字符串元素,会抛出以下的异常
java.util.ConcurrentModificationException.在增强for循环中使用add方法添加元素的时候,也会抛出该异常。这个异常出现的原因是触发了Java集合的错误检测机制——fail-fast. 当多个线程对集合进行结构上的改变的操作时,都可能产生fail-fast,抛出ConcurrentModificationException(检测到对象的并发修改,但是不允许这种修改时,就会抛出该异常)。需要注意的是,如果单线程违反了规则,也会抛出异常。
4.2.2 问题分析
将上面的diamante反编译,得到以下的代码
public static void main(String[] args) {
// 使用ImmutableList初始化一个List
List<String> userNames = new ArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("Hollis"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);
报错的代码是iterator.next()这一行,如果remove代码没有执行过,iterator.next这一行是一直没有报错的。在remove执行完之后的那一次next方法的调用才抛出异常。
Iterator.next方法调用了itertor.checkForComodification()方法,这个方法判断modCount和expectedModCount是否相等,如果不相等,那么就抛出异常。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount和expectModCount的含义如下:
- modCount是ArrayList中的一个成员变量,表示该集合实际被修改的次数
- Itr实现了Iterator接口,是一个类,使用ArrayList.iterator()方法获得的迭代器就是这个Itr类的实例
- expectedModCount是迭代器内部的一个成员变量,用来记录迭代器修改集合的次数。
然后我们调用集合的add方法和remove方法只是修改了ArrayList里的modCount,没有修改迭代器实例里面的变量expectedModCount。
所以问题就是。之所以抛出错误,是因为我们的代码使用了增强for循环,在增强for循环中,集合遍历是通过iterator进行的,遍历的时候都会检查expectedModCount和ModCount是否相等,但是元素的增加和删除用的集合类自己的方法,会改变ModCount,导致ModCount跟expectedModCount不相等。导致iterator在遍历的时候,发现两个变量不相等,这时候就抛出异常,提示用户可能发生了并发修改。
面试问题:问什么forEach不能增加删除元素
以ArrayList为例子,ArrayList有一个内部类Itr,Itr实现了Iterator接口,而且Itr内部有一个变量expectedModCount,用来记录该Itr类对象修改集合的次数。然后ArrayList里还有一个成员变量ModCount,用来记录集合被修改的次数。在运行forEach循环的时候,ArrayList创建一个Itr对象,用这个对象来遍历集合,每一次遍历集合的时候,都会判断ModCount和expectedModCount是否相同,如果不相同的话,那就抛出错误。当我们调用ArrayList自身的add和remove函数的时候,只是修改了ModCount,并没有修改迭代器的expectedModCount,导致迭代器迭代的时候发现ModCount和expectedModCount不相同,所以就报错。