Java SE常问

目录

1. 语法

1.1 final关键字的使用

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)

  1. 修饰类
      当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
  2. 修饰方法
    使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。
  3. 修饰变量
    对于一个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 重载和重写的区别(两者都是多态的表现)

  1. 如果多个方法有相同的名字,不同的参数,便产生了重载。如果两个函数已经有重载的关系,那么他们的返回类型可以不同,是否重载只看名字和参数。是编译时多态。
  2. 重写就是当子类继承自父类的相同方法,输入数据一样,但是要做出有别于父类的响应时,就需要覆盖父类方法。是运行时多态。
    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点
  1. 子类拥有父类对象的所有属性和方法(包括私有属性和私有方法),但是私有属性和私有方法是无法访问的,只是拥有而已。
2.2.2 多态
  1. 有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则,它表明子类的每个对象也是父类的对象。
  2. “is-a”规则的另一种表述是置换法则,表示程序中出现父类对象的任何地方都能用一个子类对象置换。例如可以将一个子类对象赋值给父类引用。
  3. 引用类型变量的方法调用的是哪个类的方法,必须在程序运行期间才能确定。
2.2.1 多态存在的三个前提条件
  • 继承
  • 重写
  • 父类引用指向子类对象
2.2.2 多态的两种表现形式
  • 重载
  • 多态

2.3 修饰符

2.3.1 static
2.3.1.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 注意事项
  1. 静态代码块对于定义在它后面的静态变量,可以赋值,但是不能访问。
    在这里插入图片描述
  2. 静态代码块可能在第一次new的时候执行,也可在Class.forName(“ClassDemo”) 创建Class对象的时候也会执行。

2.4 抽象类和接口的区别

没有普通的成员变量,只有常量,默认使用public static final修饰;
可以有普通的成员变量,并且可以用private修饰

接口没有静态代码块
抽象类可以有静态代码块

接口没有构造函数
抽象类有构造函数

接口是多继承的
抽象类是单继承的

从设计层面来说,抽象类是对类的抽象,是一种模板设计。接口是对行为的抽象,是一种行为的规范。

2.4.1 实现接口的类需要实现接口内的所有方法吗?

抽象类实现某个接口,可以不实现所有接口的方法,可以由它的子类实现。而普通类即非抽象类则必须实现接口里的全部方法,实现的时候可以是空实现(方法体为空没有任何作用)

2.4.2 接口的方法
  1. 接口有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方法?
  1. Java编程规定了如果两个对象根据equals方法比较是相等的,那么调用这两个对象的hashcode方法都会产生同样的结果。定义这条规则是为了提高hash表的效率,在hash表中,如果两个元素的hashcode不同,说明这两个元素不同,那就没有必要进行equals的比较了,这就大大减少了equals的比较次数。如果没有重写hashcode的话,那么相同的对象可能返回不同的hashcode,这就违反了这一条规则。
2.5.5 Java序列化中如果有字段不想进行序列化,怎么办?

对于不想进行序列化的变量,用transient来修饰。transient只能修饰变量,不能修饰类和方法。

2.5.4 Java和C++的区别

  1. Java程序编译后的代码是class文件,不能直接被硬件系统直接运行。不同的硬件平台装有不同的JVM虚拟机,由JVM来把class文件翻译成对应硬件平台能够识别的代码。
    C/C++编译后的代码能够直接在硬件平台上运行,但是编译后的代码换一个平台就不能执行了。
    所以,Java是编译文件级的跨平台,c/c++是源代码级的跨平台
  2. C++的类可多继承,Java只能单继承
  3. Java运行在虚拟机上,不需要考虑内存管理和垃圾回收机制。但是C++需要考虑。

3. Java核心技术

3.1 反射机制

Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的所有方法和获取其属性的值。这种动态获取信息和动态调用对象的方法的功能成为java语言的反射机制。

3.1.1 获取Class文件的四种方法
  1. class clazz=class.forName(" 权限类名");

  2. class clazz=类名.class;

  3. class clazz=对象.getclass();

  4. 通过类加载器获得

3.1.2 反射机制的优缺点
  • 优点:运行期判断类型,动态加载类,可以提高代码的灵活度、
  • 缺点:1. 性能瓶颈:反射相当于一系列解释操作,性能比直接的Java代码慢很多。2. 安全问题,因为可以访问私有的属性和方法,增加了类的安全隐患。
3.1.3 反射的主要用途
  • 用于IDE,当我们输入一个对象或类并想调用它的属性和方法时,IDE就会自动列出它的属性和方法。
  • 很多框架(如spring)都是配置化的,为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这时候用到反射,运行时动态加载对象。
3.1.4 为什么反射比较慢
  1. 因为反射是在运行时而不是在编译时,所以不会利用到编译优化。同时因为是动态生成,因此反射比较慢。(待验证)

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不会被执行

  1. finally语句块的第一行发生了错误
  2. 在异常语句前使用了System.exit(int),这时候已经退出了程序,finally不会执行。如果System.exit(int)在异常语句之后,那么finally会执行。
  3. 程序所在线程死亡
  4. CPU被关闭。
3.2.4 使用try-with-resources来代替try-catch-finally
  1. 使用范围是:任何实现了AutoCloseable接口或者Closeable接口的对象
  2. 关闭资源和finally的执行顺序:在try-with-resources语句中,catch块和finally块在声明的资源关闭后再运行。

《Effective Java》指出:
面对必须关闭的资源,应该优先使用try-with-resources而不是try-finally。随之产生的代码更简短,更清晰,产生的异常更有用。用try-catch-finally可能导致很多问题。

3.3 多线程

在这里插入图片描述
在这里插入图片描述

  1. yield方法
    放弃当前的CPU资源,让给其他任务去执行。当时放弃的时间不确定,有可能刚刚放弃,又马上获得。
  2. 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不相同,所以就报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值