Java 知识点(持续更新中)

本文仅对下列问题进行简单地阐述,若有错误请多包涵

Java 基础

类初始化顺序

父类 static 代码块 –> 子类 static 代码块 –> 父类 non-static 代码块 –> 父类构造器 –> 子类 non-static 代码块 –> 子类构造器


##面向对象的特征
三大特性:继承、封装和多态(另有四大特性,加上抽象)、

封装

在面向对象语言中,封装特性是由类来体现的,我们将现实生活中的一类实体定义成类,其中包括属性和行为(在 Java 中就是方法),就好像人类,可以具有 name, sex, age 等属性,同时也具有 eat(), sleep() 等行为,我们在行为中实现一定的功能,也可操作属性,这是面向对象的封装特性

继承

继承就像是我们现实生活中的父子关系,儿子可以遗传父亲的一些特性,在面向对象语言中,就是一个类可以继承另一个类的一些特性,从而可以代码重用,其实继承体现的是 is-a 关系,父类同子类在本质上还是一类实体

多态

多态就是通过传递给父类对象引用不同的子类对象从而表现出不同的行为

抽象

抽象就是将一类实体的共同特性抽象出来,封装在一个抽象类中,所以抽象在面向对象语言是由抽象类来体现的。比如鸟就是一个抽象实体,因为抽象实体并不是一个真正的对象,它的属性还不能完全描述一个对象,所以在语言中体现为抽象类不能实例化


final,finally,finalize 的区别

final

final 用于修饰类、成员变量和成员方法

final 修饰的类不能被继承(如 String),其中所有的方法都不能被重写

final 修饰的方法不能被重写,但是子类可以用父类中 final 修饰的方法

final 修饰的变量是不可变的,若该变量是基本数据类型,初始化之后该变量的值不能被改变。若该变量是引用类型,则其只能指向初始化指向的那个对象

方法内声明的类或者方法内的匿名内部类,访问该方法内定义的变量,该变量必须要用final修饰。当内部类访问局部变量时,会扩大局部变量的作用域,如果局部变量不用 final 修饰,我们就可以在内部类中随意修改该局部变量值,而且是在该局部变量的作用域范围之外可以看到这些修改后的值,会出现安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
final String str = "hello";

new Thread(new Runnable() {

@Override
public void run() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(str);
}
}
}).start();

System.out.println("main thread finished");
}

finally

通常和 try catch 搭配使用,保证不管有没有发生异常,资源都能够被释放(如释放连接、关闭 IO 流等)

finally 内的语句发生在 try 语句块中 return 语句之前

finalize

是 object 类中的一个方法,子类可以重写 finalize() 方法实现对资源的回收。垃圾回收只负责回收内存,并不负责资源的回收,资源回收要由程序员完成,Java 虚拟机在垃圾回收之前会先调用垃圾对象的 finalize 方法用于使对象释放资源(如关闭连接、关闭文件),之后才进行垃圾回收,这个方法一般不会显示的调用,在垃圾回收时垃圾回收器会主动调用


Exception、Error、运行时异常与一般异常有何异同

Throwable 是所有 Java 程序中错误处理的父类,有两个子类:Error 和 Exception

Error

表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息

Error 类体系描述了 Java 运行系统中的内部错误以及资源耗尽的情形。应用程序不应该抛出这种类型的对象(一般是由虚拟机抛出)

假如出现这种错误,除了尽力使程序安全退出外,在其他方面是无能为力的

Exception

表示可恢复的例外,这是可捕捉到的

Java 提供了两类主要的异常:runtime exception 和 checked exception

####CheckedException
checked 异常也就是我们经常遇到的 IO 异常,以及 SQL 异常都是这种异常。对于这种异常,Java 编译器强制要求我们必须对出现的这些异常进行 catch。所以,面对这种异常不管我们是否愿意,只能自己去写一大堆 catch 块去处理可能的异常。这类异常一般是外部错误,例如试图从文件尾后读取数据等,这并不是程序本身的错误,而是在应用环境中出现的外部错误.

RuntimeException

也称运行时异常,我们可以不处理。当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过NullPointerException 异常,它就是运行时异常,并且这种异常还是最常见的异常之一。RuntimeException 体系包括错误的类型转换、数组越界访问和试图访问空指针等等

处理 RuntimeException 的原则是:假如出现 RuntimeException,那么一定是程序员的错误。例如,可以通过检查数组下标和数组边界来避免数组越界访问异常

运行时异常

出现运行时异常后,系统会把异常一直往上层抛,一直遇到处理代码。如果没有处理块,到最上层,如果是多线程就由Thread.run() 抛出,如果是单线程就被 main() 抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了

运行时异常是 Exception 的子类,也有一般异常的特点,是可以被 Catch 块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止

如果不想终止,则必须扑捉所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。在这个场景这样处理可能是一个比较好的应用,但并不代表在所有的场景你都应该如此。如果在其它场景,遇到了一些错误,如果退出程序比较好,这时你就可以不太理会运行时异常,或者是通过对异常的处理显式的控制程序退出。异常处理的目标之一就是为了把程序从异常中恢复出来


常见的 runtime exception

NullPointerException - 空指针引用异常

ClassCastException - 类型强制转换异常。

IllegalArgumentException - 传递非法参数异常。

ArithmeticException - 算术运算异常

ArrayStoreException - 向数组中存放与声明类型不兼容对象异常

IndexOutOfBoundsException - 下标越界异常

NegativeArraySizeException - 创建一个大小为负数的数组错误异常

NumberFormatException - 数字格式异常

SecurityException - 安全异常

UnsupportedOperationException - 不支持的操作异常

EOFException - 文件已结束异常

FileNotFoundException - 文件未找到异常

更多的异常见链接 runtime exception


int 和 Integer 的区别

基本使用对比

Integer 是 int 的包装类,int 是基本数据类型

Integer 变量必须实例化后才能使用,int 变量不需要

Integer 实际是对象的引用,指向此 new 的 Integer 对象,int 是直接存储数据值

Integer 的默认值是 null,int 的默认值是 0

深入对比

由于 Integer 变量实际上是对一个 Integer 对象的引用,所以两个通过 new 生成的 Integer 变量永远是不相等的(因为 new 生成的是两个对象,其内存地址不同)

1
2
3
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

Integer 变量和 int 变量比较时,只要两个变量的值是相等的,则结果为 true(因为包装类 Integer 和基本数据类型 int 比较时,Java 会自动拆包装为 int,然后进行比较,实际上就变为两个 int 变量的比较)

1
2
3
Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

非 new 生成的 Integer 变量和 new Integer() 生成的变量比较时,结果为false(因为非 new 生成的 Integer 变量指向的是 Java 常量池中的对象,而 new Integer() 生成的变量指向堆中新建的对象,两者在内存中的地址不同)

1
2
3
Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

对于两个非 new 生成的 Integer 对象,进行比较时,如果两个变量的值在区间 -128 到 127 之间,则比较结果为 true,如果两个变量的值不在此区间,则比较结果为 false

1
2
3
4
5
6
7
Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true

Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

Integer 的值缓存范围

Java 在编译 Integer i = 100 时,会翻译成为 Integer i = Integer.valueOf(100)。而 Java API 中对 Integer 类型的 valueOf 的定义如下,对于 -128 到 127 之间的数,会进行缓存,Integer i = 127 时,会将 127 进行缓存,下次再写 Integer j = 127 时,就会直接从缓存中取,就不会 new 了。

1
2
3
4
5
6
7
public static Integer valueOf(int i){
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high){
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}

IntegerCache 是 Integer 的内部类,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 缓存支持自动装箱的对象标识语义
* -128和127(含)。
*
* 缓存在第一次使用时初始化。 缓存的大小
* 可以由-XX:AutoBoxCacheMax = <size>选项控制。
* 在VM初始化期间,java.lang.Integer.IntegerCache.high属性
* 可以设置并保存在私有系统属性中
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}

private IntegerCache() {}
}


Java 数据类型

分为基本数据类型和引用数据类型

基本数据类型:boolean、byte、char、short、int、long、float、double

引用数据类型:数组、类、接口

包装类

为了能够将这些基本数据类型当成对象操作,Java 为每 一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。

封装类类型:Boolean、Byte、Character、Short、Integer、Long、Float、Double


装箱和拆箱

自动装箱

将基本数据类型重新转化为对象

1
2
3
4
5
6
7
8
public class Test {  
public static void main(String[] args) {
//声明一个Integer对象
Integer num = 9;

//以上的声明就是用到了自动的装箱:解析为:Integer num = new Integer(9)
}
}

自动拆箱

将对象重新转化为基本数据类型

1
2
3
4
5
6
7
8
9
public class Test {  
public static void main(String[] args) {
//声明一个Integer对象
Integer num = 9;

//进行计算时隐含的有自动拆箱
System.out.println(num--);
}
}

因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除


String、StringBuffer、StringBuilder

运行速度

StringBuilder > StringBuffer > String

String 为字符串常量,而 StringBuilder 和 StringBuffer 均为字符串变量

Java 中对 String 对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢

但在某些情况下,JVM 会对 String 操作进行优化

1
2
3
4
5
6
//JVM会将其优化为 String str = "abcde”;
String str = "abc" + "de"; //快
StringBuilder stringBuilder = new StringBuilder().append("abc").append("de"); //较快
String str1 = "abc";
String str2 = "de";
String string = str1 + str2; //慢

线程安全

StringBuilder 是线程不安全的,而 StringBuffer 是线程安全的

如果一个 StringBuffer 对象在字符串缓冲区被多个线程使用时,StringBuffer 中很多方法可以带有 synchronized 关键字,所以可以保证线程是安全的,但 StringBuilder 的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用 StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的 StringBuilder


重载和重写的区别

重载

是静态分派的典型应用(JVM 知识)

在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序

不能通过访问权限、返回类型、抛出的异常进行重载;

方法的异常类型和数目不会对重载造成影响;

对于继承来说,如果某一方法在父类中是访问权限是priavte,那么就不能在子类对其进行重载,如果定义的话,也只是定义了一个新方法,而不会达到重载的效果。

重写(覆盖)

和动态分派有很大关系(JVM知识)

覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;

覆盖的方法的返回值必须和被覆盖的方法的返回一致;

覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;

被覆盖的方法不能为 private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖

需要关注的点

JVM 在重载时是通过参数的静态类型而不是实际类型作为判定依据的。而静态类型是编译期可知的。因此在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载函数(注意与多态、重写的区别)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Father {

public void test() {
System.out.println("this is father");
}

}

class Child extends Father {

@Override
public void test() {
System.out.println("this is child");
}
}

public class TestOverload {

public void test(Father father) {
System.out.println("this is father test");
}

public void test(Child child) {
System.out.println("this is child test");
}

public static void main(String[] args) {
Father father = new Child();
father.test(); // this is child
TestOverload testOverload = new TestOverload();
testOverload.test(father); // this is father test
}

}


接口和抽象类的联系和区别

相似性

接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承

接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法

区别

接口里只能包含抽象方法,静态方法和默认方法(default 修饰),不能为普通方法提供方法实现,抽象类则完全可以包含普通方法

接口里只能定义静态常量(默认修饰符为 public static final),不能定义普通成员变量,抽象类里则既可以定义普通成员变量,也可以定义静态常量

接口不能包含构造器,抽象类可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作

接口里不能包含初始化块,但抽象类里完全可以包含初始化块

一个类最多只能有一个直接父类,包括抽象类,但一个类可以直接实现多个接口,通过实现多个接口可以弥补 Java 单继承不足


反射

允许运行中的 Java 程序获取自身的信息,并且可以操作类和对象的内部属性

核心是 JVM 在运行时才动态加载的类或调用方法或属性,JVM 不需要事先知道运行对象是谁

所有的类都是再对其第一次使用时,动态加载到 JVM 中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法

功能

在运行时判断任意一个对象所属的类

在运行时构造任意一个类的对象

在运行时判断任意一个类所具有的成员变量和方法(通过反射设置可以调用 private)

在运行时调用任意一个对象的方法

用途

获取 Class 对象

判断是否为某个类的实例

  • 一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 Native 方法

创建实例

  • 使用 Class 对象的 newInstance() 方法来创建对象对应类的实例
  • 先通过 Class 对象获取制定的 Constructor 对象,在调用 Constructor 对象的 newInstance() 方法来创建实例。这种方法可以用指定的构造器构造类的实例

IDE 的代码提示,”.” 号之后 IDE 会自动列出相应的属性或方法

各种框架

获取 Class 类的对象的方法

1
2
3
4
Class clazz = Class.forName("com.liqingyu.jdbc.Person");
Person person = new Person();
clazz = person.getClass(); //其中 person 为 Person 类的一个对象
clazz = Person.class;

建议使用 .class 方式调用,此做法更简单、安全,在编译器就会受到检查

使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象,此例中即不会执行 Person 类中的静态代码块,包括构造器

通过 Class 类的对象来创建所需类的对象

class.newInstance()

其中 class 是 Class 类的一个对象

通过 newInstance() 来创建的类,必须带有默认的构造器

泛化的 Class 引用

1
2
Class< Integer > intClass = int.class;
intClass = double.class //Illegal

使用通配符放宽限制

1
2
3
4
5
6
7
Class< ? > intClass = int.class;
intClass = double.class;


------
Class< ? extends Number > numClass = int.class;
numClass = double.class;

向 Class 引用添加泛型语法的原因仅仅是为了提供编译期类型检查


自定义注解的场景及实现

根据注解参数的个数,注解可分为:标记注解、单值注解、完整注解

注解的作用

生成文档。这是最常见的,也是 Java 最早提供的注解。常用的有 @see @param @return 等

跟踪代码依赖性,实现替代配置文件功能。比较常见的是 spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量

在编译时进行格式检查。如 @override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出

注解的实现

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
public int id();
public String desc() default "no descriptions";
}

HTTP 请求的 GET 和 POST 方式的区别


由于 HTTP 协议是无状态的协议,故需要某种机制来保持状态,而 Session 和 Cookie 应运而生

session 存储在服务器端,而 cookie 存储在客户端

session 的运行以来 session id,而 session id 存在 cookie 中


hashCode 和 equals 方法的区别和联系

hashCode 和 equals 方法均在 Object 类中定义

equals (Object obj) 方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回 bfalse。hashCode() 方法返回一个 int 数,在 Object 类中的默认实现是“将该对象的内部地址转换成一个整数返回”

若重写了 equals(Object obj) 方法,则有必要重写 hashCode() 方法

  • 一般一个类的对象如果会存储在 HashTable,HashSet,HashMap 等散列存储结构中,那么重写 equals 后最好也重写 hashCode,否则会导致存储数据的不唯一性(存储了两个equals 相等的数据)

若 equals 返回 true,则 hashCode 返回的 int 值相等。但若 hashCode 返回的 int 值相等,equals 返回不一定为 true

hashCode 是为了提高在散列结构存储中查找的效率,在线性表中没有作用

同一对象在执行期间若已经存储在集合中,则不能修改影响 hashCode 值的相关信息,否则会导致内存泄露问题


什么是 Java 序列化和反序列化,如何实现 Java 序列化?或者请解释 Serializable 接口的作用

序列化和反序列化

对象序列化机制(object serialization)是 Java 语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在 JVM 中的活动对象和字节数组(流)之间进行转换

对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

如何实现 Java 序列化

在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化

通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

序列化并不保存静态变量

要想将父类对象也序列化,就需要让父类也实现Serializable 接口

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null

服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全

补充

如果一个类想被序列化,需要实现 Serializable 接口。否则将抛出 NotSerializableException 异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种

在变量声明前加上该关键字,可以阻止该变量被序列化到文件中

在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略

有关 ArrayList 的序列化和反序列化见链接


Object 类中常见的方法,为什么 wait、notify 会放在 Object 里面?

equals、getClass、hashCode、notify、toString、wait 方法等

为什么 wait、notify 会放在 Object 里面

wait(),notify(),notifyAll() 都必须使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁

简单说:因为 synchronized 中的这把锁可以是任意对象,所以任意对象都可以调用 wait() 和 notify();所以 wait 和 notify 属于 Object。

专业说:因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的 notify 唤醒,不可以对不同锁中的线程进行唤醒。


Java 的平台无关性如何体现出来

最主要的是 Java 平台本身。Java 平台扮演 Java 程序和所在的硬件与操作系统之间的缓冲角色。这样 Java 程序只需要与 Java 平台打交道,而不用管具体的操作系统

Java 语言保证了基本数据类型的值域和行为都是由语言自己定义的。而 C/C++ 中,基本数据类是由它的占位宽度决定的,占位宽度由所在平台决定的。不同平台编译同一个 C++ 程序会出现不同的行为。通过保证基本数据类型在所有平台的一致性,Java 语言为平台无关性提供强有力的支持

Java class 文件。Java 程序最终会被编译成二进制 class 文件。class 文件可以在任何平台创建,也可以被任何平台的 Java 虚拟机装载运行。它的格式有着严格的定义,是平台无关的

可伸缩性。Sun 通过改变 API 的方式得到三个基础 API 集合,表现为 Java 平台不同的伸缩性:J2EE,J2SE,J2ME


JDK 和 JRE 的区别

JDK 是 Java 的开发工具,它不仅提供了 Java 程序运行所需的 JRE,还提供了一系列的编译,运行等工具,如 javac,java,javaw 等。JRE 只是 Java 程序的运行环境,它最核心的内容就是 JVM(Java 虚拟机)及核心类库


Java 8 有哪些特性

详细内容见链接

default 方法和 static 方法

在接口中新增了 default 方法和 static 方法,这两种方法可以有方法体

  • 接口里的静态方法,即 static 修饰的有方法体的方法不会被继承或者实现,但是静态变量会被继承
  • default 方法可以被子接口继承亦可被其实现类所调用
  • default 方法被继承时,可以被子接口覆写
  • 如果一个类实现了多个接口,且这些接口中无继承关系,这些接口中若有相同的(同名,同参数)的 default 方法,则接口实现类会报错,接口实现类必须通过特殊语法指定该实现类要实现那个接口的 default 方法。特殊语法:<接口>.super.<方法名>([参数])

Lambda 表达式

Lambda 表达式可以看成是匿名内部类,使用Lambda表达式时,接口必须是函数式接口

1
2
3
<函数式接口>  <变量名> = (参数1,参数2...) -> {
//方法体
}

函数式接口

如果一个接口只有一个抽象方法,则该接口称之为函数式接口,因为 默认方法不算抽象方法,所以你也可以给你的函数式接口添加默认方法

Lambda 作用域

访问局部变量

访问对象字段与静态变量

lambda内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的

访问接口的默认方法

Date API

Annotation 注解


List、Set、Map 的区别

List 可以有重复的对象,有序的,每个对象都带下标

List 可以插入多个 null 元素

Set 为无序容器,不可以有重复的对象,Map 是键值成对的

List Set Map 都是接口,前两个继承至 Collection 接口,Map 为独立接口

Set 下有 HashSet,LinkedHashSet,TreeSet

List 下有 ArrayList,Vector,LinkedList

Map 下有 Hashtable,LinkedHashMap,HashMap,TreeMap


ArrayList 和 LinkedList 区别

ArrayList

和 Vector 不同,ArrayList 中的操作是非线程安全的

ArrayList 实际上是通过一个数组去保存数据的,当我们构造 ArrayList 时,如果使用默认构造函数,最后 ArrayList 的默认容量大小是 10

当 ArrayList 容量不足以容纳全部元素时,ArrayList 会自动扩张容量,新的容量 = 原始容量 + 原始容量 / 2

LinkedList

1
2
3
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

区别

LinkedList 是一个继承于 AbatractSequentialList 的双向链表。它也可以被当作堆栈、队列或双端队列进行操作

无论如何,千万不要用随机访问去遍历 LinkedList

ArrayList 是实现了基于动态数组的数据结构,而LinkedList 是基于链表的数据结构;

对于随机访问 get 和 set,ArrayList 要优于 LinkedList,因为 LinkedList 要移动指针;

对于添加和删除操作 add 和 remove,一般大家都会说 LinkedList 要比 ArrayList 快,因为 ArrayList 要移动数据。但是实际情况并非这样,对于添加或删除,LinkedList 和 ArrayList 并不能明确说明谁快谁慢

ArrayList 想要在指定位置插入或删除元素时,主要耗时的是 System.arraycopy 动作,会移动 index 后面所有的元素;LinkedList 主耗时的是要先通过 for 循环找到 index,然后直接插入或删除。这就导致了两者并非一定谁快谁慢


ArrayList 与 Vector 的区别

Vector 是线程安全的,源码中有很多的 synchronized 可以看出,而 ArrayList 不是。导致 Vector 效率无法和 ArrayList 相比;

ArrayList 和 Vector 都采用线性连续存储空间,当存储空间不足的时候,ArrayList 默认增加为原来的50%,Vector 默认增加为原来的一倍;

Vector 可以设置 capacityIncrement,而 ArrayList 不可以,从字面理解就是 capacity 容量,Increment 增加,容量增长的参数

计算机网络

BIO、NIO、AIO 的概念

BIO

同步阻塞式 IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善

通过线程池机制可以优化 BIO 模型,达到伪异步 IO

NIO

同步非阻塞式 IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理

AIO(NIO2.0)

异步非阻塞式 IO,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理


什么是长连接和短连接

短连接:连接->传输数据->关闭连接

长连接:连接->传输数据->保持连接 -> 传输数据-> ………..->直到一方关闭连接,多是客户端关闭连接

HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接


单个 UDP 报文最大容量

从 MTU 角度看

以太网(Ethernet)数据帧的长度必须在 46-1500 字节之间,这是由以太网的物理特性决定的。这个 1500 字节被称为链路层的 MTU(最大传输单元)。但这并不是指链路层的长度被限制在 1500 字节,其实这个 MTU 指的是链路层的数据区。并不包括链路层的首部和尾部的 18 个字节。所以,事实上,这个 1500 字节就是网络层 IP 数据报的长度限制。因为 IP 数据报的首部为 20 字节,所以 IP 数据报的数据区长度最大为 1480 字节.而这个 1480 字节就是用来放 TCP 传来的 TCP 报文段或 UDP 传来的 UDP 数据报的.又因为 UDP 数据报的首部8字节,所以 UDP 数据报的数据区最大长度为 1472 字节。这个 1472 字节就是我们可以使用的字节数

当我们发送的 UDP 数据大于 1472 的时候会怎样呢?这也就是说 IP 数据报大于 1500 字节,大于 MTU。这个时候发送方 IP 层就需要分片 。把数据报分成若干片,使每一片都小于 MTU.而接收方 IP 层则需要进行数据报的重组.这样就会多做许多事情,而更严重的是,由于 UDP 的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个 UDP 数据报。

进行 Internet 编程时则不同,因为 Internet 上的路由器可能会将 MTU 设为不同的值.如果我们假定 MTU 为 1500 来发送数据的,而途经的某个网络的 MTU 值小于 1500 字节,那么系统将会使用一系列的机制来调整 MTU 值,使数据报能够顺利到达目的地,这样就会做许多不必要的操作

鉴于 Internet 上的标准 MTU 值为 576 字节,建议在进行 Internet 的 UDP 编程时.最好将 UDP 的数据长度控件在 548 字节 (576-8-20) 以内

从IP封包总长度看

在IP头中,封包总长度为 2 bytes 也就是一个 IP 包最多长度为 65535 个,这样减去一个 I P头,再减一个 UDP 头就是 65535- 20 - 8 = 65507 个。

总结

从上面两个来看,UDP 编程时最好不要超过 1472(针对以太网),如果要求不高,就算超过了 1472 了,也不可以超过 65507,即包总大小不要超过 64k