1、Java 语言有哪些特点?
1)简单易学; 2)面向对象(封装,继承,多态); 3)平台无关性(Java 虚拟机实现平台无关性,即针对不同操作系统都有其特定实现(Windows,Linux,macOS)); 4)具有可靠性;安全性; 5)支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持,屏蔽了复杂的底层实现); 6)支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); 7)编译与解释并存;
2、面向对象和面向过程的区别
- 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。 - 参见 issue : 面向过程 :面向过程性能比面向对象高?
这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。 面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。
3、介绍下JVM、JDK、JRE。
1) JVM
- Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
- 什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。 Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - Java 程序从源代码到运行一般有下面 3 步:
- 我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。 而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
HotSpot 采用了**惰性评估(Lazy Evaluation)**的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。 但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
- 总结:
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同操作系统的 JVM 实现是 Java 语言“一次编译,随处运行”的关键所在。
4、字符型常量和字符串常量的区别?
- 形式上: 字符常量(char类型)是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
- 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
- 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节)
5、switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上
在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以作用在switch上。
6、short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1+ 1);其中有隐含的强制类型转换。
7、&和&&的区别
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true,整个表达式的值才是 true。 &&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 而逻辑与&通常会用来做整数之间的运算,来加快计算结果。
(n << 1)^ 1
n & (~n+1)
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
8、== 和equals方法
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型 比较的是具体的数值,引用数据类型 比较的是内存地址)。 equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
举个例子:
public class test1 {
public static void main(String[] args) {
String a = new String("ab");
String b = new String("ab");
String aa = "ab";
String bb = "ab";
if (aa == bb)
System.out.println("aa==bb");
if (a == b)
System.out.println("a==b");
if (a.equals(b))
System.out.println("aEQb");
if (42 == 42.0) {
System.out.println("true");
}
}
}
说明:
- String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
9、int和Integer
为了编程的方便,引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型: 原始类型: boolean,char,byte,short,int,long,float,double 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double 说出下面程序的输出结果:
int i = 120;
int j = 120;
System.out.println(i == j);
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println("-----------");
Integer x = 600;
Integer y = 600;
System.out.println(x == y);
System.out.println(x.equals(y));
解析见阿里巴巴的开发手册中OOP 规约:
【强制】 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。 说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
10、final有什么作用?
final 关键字主要用在三个地方:变量、方法、类。
-
对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 -
被final修饰的方法不可以被重写,使用 final 方法的原因有两个。第一个原因是把方法锁定,防止任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。 -
被final修饰的类不可以被继承 。final 类中的所有成员方法都会被隐式地指定为 final 方法,比如,java.lang包下的String类。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
public boolean equals(Object anObject) {
}
}
11、break ,continue ,return 的区别及作用
break 跳出总上一层循环,不再执行循环(结束当前的循环体) continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) return 结束程序,不再执行下面的代码(结束当前的方法,直接返回)
12、在 Java 中,如何跳出当前的多重嵌套循环?
在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号/标签,然后在 里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:
public static void main(String[] args) {
ok:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i=" + i + ",j=" + j);
if (j == 5) {
break ok;
}
}
}
}
13、Java 中都有哪些异常?
- Java 异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception (异常)和 Error (错误)。Exception 能被程序本身处理(try-catch ), Error 是无法处理的(只能尽量避免)。
Exception 和 Error 二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError )、虚拟机内存不够错误(OutOfMemoryError )、类定义错误(NoClassDefFoundError )等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
受检查异常
Java 代码在编译过程中,如果受检查异常没有被 catch /throw 处理的话,就没办法通过编译 。比如下面这段 IO 操作的代码。 除了RuntimeException 及其子类以外,其他的Exception 类及其子类都属于检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException 等。
不受检查异常
Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常,例如:NullPoin?terException 、NumberFormatException (字符串转换为数字)、ArrayIndexOutOfBoundsException (数组越界)、ClassCastException (类型转换错误)、ArithmeticException (算术错误)等。
14、Java中的异常处理
try 块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。catch 块: 用于处理 try 捕获到的异常。finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
除此之外,finally块主要用于回收在try块里打开资源(如数据库连接、网络连接和磁盘文件)。finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了 return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
在以下 3 种特殊情况下,finally 块不会被执行:
1) 在 try 或 finally 块中用了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行 2) 程序所在的线程死亡。 3) 关闭 CPU。 再次强调: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。 如下:
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
如果调用 f(2) ,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。
处理异常除了可以用try-catch-finally进行捕获外,还可以抛出和声明异常,交给 可能存在异常的方法的调用者来处理异常。
- throw:用于抛出异常。
- throws:用在方法签名中,用于声明该方法可能抛出的异常。
15、重载和重写的区别,重载的方法能否根据返回类型进行区分?
- 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
- 重写:
1)返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类(里氏代换原则)。 2)如果父类方法访问修饰符为 private/final/static , 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 3)构造方法无法被重写。
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。 注意: 关于重写的返回值类型,需要注意的是,如果方法的返回类型是void和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
public class Hero {
public String name() {
return "超级英雄";
}
}
public class SuperMan extends Hero{
@Override
public String name() {
return "超人";
}
public Hero hero() {
return new Hero();
}
}
public class SuperSuperMan extends SuperMan {
public String name() {
return "超级超级英雄";
}
@Override
public SuperMan hero() {
return new SuperMan();
}
}
16、说下Java 面向对象编程三大特性。
1) 封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
2)继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法(重写)。
3)多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
17、String、StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[] ,所以 String 对象是不可变的。
在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串 private final byte[] value
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的。 AbstractStringBuilder.java :
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity 、append 、insert 、indexOf 等公共方法。 StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
}
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String - 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder - 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
18、接口和抽象类的区别是什么?
1) 接口的方法默认是 public ,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 2)接口中除了 static 、final 变量,不能有其他变量,而抽象类中则不一定。 3)一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。 4)接口方法默认修饰符是 public ,抽象方法可以有 public 、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰)。 5)从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
备注:
- 在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。
- jdk9 的接口被允许定义私有方法 。
总结一下 jdk7~jdk9 Java 中接口概念的变化(相关阅读):
- 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
- jdk 8 的时候接口可以有默认方法和静态方法功能。
- Jdk 9 在接口中引入了私有方法和私有静态方法。
19、Comparable 和 Comparator的区别?
-
Comparable接口实际上是出自java.lang包,它有一个 compareTo(Objectobj)方法可以由程序员来定义比较策略。 -
Comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1,Object obj2)方法可以由程序员来定义比较策略。
一般,比较自定义实体类的某个属性的大小需要实现Comparable 和Comparator接口。
实现Comparator接口,在实体类之外另外声明自定义比较器:
public class MyComparator {
public static class User {
private int uid;
private String uname;
private int age;
public User(int uid, String uname, int age) {
this.uid = uid;
this.uname = uname;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", uname='" + uname + '\'' +
", age=" + age +
'}';
}
}
public static class DescByUidComparator implements Comparator<User> {
@Override
public int compare(User o1, User o2) {
return o2.uid - o1.uid;
}
}
public static class AscByAgeComparator implements Comparator<User>{
@Override
public int compare(User o1, User o2) {
return o1.age -o2.age;
}
}
public static class UidAscAndAgeDec implements Comparator<User>{
@Override
public int compare(User o1, User o2) {
return o1.uid != o2.uid ? (o1.uid - o2.uid):(o2.age -o1.age);
}
}
public static class UnameAscAndLengthAsc implements Comparator<User>{
@Override
public int compare(User o1, User o2) {
return o1.uname != o2.uname ? o1.uname.compareTo(o2.uname) : (o2.uname.length() -o1.uname.length());
}
}
public static void printArray(User[] arr){
if (arr == null || arr.length == 0){
return;
}
for (int i = 0;i < arr.length;i++){
System.out.println(arr[i]+ " ");
}
System.out.println();
}
public static void main(String[] args) {
User user1 = new User(101,"cc",46);
User user2 = new User(102,"ee",32);
User user3 = new User(103,"baaa",43);
User user4 = new User(104,"fff",54);
User user5 = new User(105,"abcd",18);
User[] users = new User[]{user1, user2, user3, user4, user5};
Arrays.sort(users,new DescByUidComparator());
printArray(users);
System.out.println(" 按照年龄age 升序排序:");
Arrays.sort(users,new AscByAgeComparator());
printArray(users);
System.out.println("编号进行升序排序,在编号相同的情况下,按照年龄进行降序排序");
Arrays.sort(users,new UidAscAndAgeDec());
printArray(users);
Arrays.sort(users,new UnameAscAndLengthAsc());
}
}
实现Comparable接口,在实体类中定义比较策略:
public class Student implements Comparable<Student>{
private int sid;
private int age;
private String name;
public Student() {
}
public Student(int sid, int age, String name) {
this.sid = sid;
this.age = age;
this.name = name;
}
public int getSid() {
return sid;
}
public void setSid(int sid) {
this.sid = sid;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestCustomSort{" +
"sid=" + sid +
", age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
return this.age == o.age ? o.name.compareTo(this.name) : this.age - o.age;
}
public static void main(String[] args) {
Student s1 = new Student(101,31,"cc");
Student s2 = new Student(102,26,"ee");
Student s3 = new Student(103,21,"aa");
Student s4 = new Student(104,31,"ca");
Student s5 = new Student(105,21,"ff");
TreeSet<Student> treeSet = new TreeSet<>();
treeSet.add(s1);
treeSet.add(s2);
treeSet.add(s3);
treeSet.add(s4);
treeSet.add(s5);
for (Student student : treeSet) {
System.out.println(student);
}
}
}
总结:
- 实现Comparable接口,在实体类中定义比较策略,compareTo方法只能定义一种比较规则,如果添加别的比较规则,只能修改compareTo中代码逻辑。
- 实现Comparator接口,在实体类之外另外声明自定义比较器,代码更易于维护,扩展性好,如果需要增加其他的比较策略,只需要另外再定义一个比较器类来实现Comparator接口,不要再修改实体类。
20、Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient 关键字修饰。 transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
21、Java 中 IO 流
1)Java 中 IO 流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流。
Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图: 按操作对象分类结构图:
2)既然有了字节流,为什么还要有字符流?
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。 所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。 如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
22、BIO、NIO、AIO有什么区别?
- **BIO (Blocking I/O):同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。**在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。
23、深拷贝和浅拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
|