Java基础复习整理

Posted by SH on October 18, 2019

Java基础

  • 基本语法:
  • 面向对象编程:封装性、重载、权限修饰、方法的参数传递机制、构造器、this关键字;
  • 高级类特性:继承性、重写、super关键字、多态性、object类、equals()、包装类;
  • 高级类特性:static、单例模式、final、抽象类与抽象方法、接口、内部类、匿名类;
  • 异常处理:异常Throwable体系结构、try_catch_finally、抛异常、自定义异常类;
  • Java集合:List、Set、Map;
  • 泛型:集合使用泛型、自定义泛型类和泛型方法、通配符;
  • 枚举&注解:枚举类、JDK内置注解类型、自定义注解、元注解;
  • IO:File类、IO流、缓冲流、字节字符流、转换流、打印流、数据流、对象流;
  • 多线程:Thread类、线程同步、线程通信;
  • Java常用类:String及内存解析、StringBuffer、StringBuilder、System、Date、Math;
  • Java反射机制:Class类、ClassLoader、反射获取类、动态代理与AOP;
  • 网络编程:InetAddress类、TCP_UDP、TCP_IP、UDP_IP、URL编程。

1.JDK 、 JRE、JVM

JDK(Java Development Kit)是Java开发工具箱,它是功能齐全的Java SDK。 它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。

JRE(Java Runtime Environment) 是 Java运行时环境。 它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。

JVM(Java Virtual Machine)(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。

  • JDK包含JRE和Java编译、开发工具;
  • JRE包含JVM和Java核心类库;
  • 运行Java仅需要JRE;而开发Java需要JDK。

如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。 如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。 但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。 例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。 那你为什么需要JDK呢? 因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

2.面向对象

面向对象编程的三条主线

1)类及类的构成成分:属性、方法、构造器、代码块、内部类;

2)面向对象编程的特征:封装性、继承性、多态性,(抽象性);

3)其他关键字:this、super、package、import、static、final、abstract、interface…

属性:

①变量的分类:

成员变量(属性Field)vs 局部变量(方法形参、方法内部、代码块内部)。

基本数据类型(8种,不同的数据类型对应不同的默认初始化值) vs 引用数据类型(数组、类、接口,默认初始化值为null)。

②属性的声明格式:修饰符 数据类型 变量名 = 初始化值;// Java是强数据类型的语言。

③对属性的赋值操作:默认初始化、显示的初始化、代码块的初始化、构造器的初始化、调用方法对属性进行赋值。

方法

①格式:修饰符(其他关键字:static/final/abstract) 返回值类型 方法名(形参列表){//方法体}

②方法的重载(overload)vs 方法的重写(override overwrite)

③方法的参数传递机制:值传递

构造器

①构造器的作用:创建类的对象;初始化对象的成员变量

②构造器也是可以重载的。

代码块

主要作用:用来初始化类的成员变量

分类:静态代码块 vs 非静态代码块

内部类

分类:成员内部类(static的成员 vs 非static的成员)vs 局部内部类(方法内部声明的类)

类的初始化

内存解析:

栈:局部变量、对象的引用名、数组的引用名;

堆:new 出来的“东西”

方法区:包名、类名、方法定义、(字符串常量池)

静态域:存放类中静态的变量

img

1)三大特性

面向对象的三个特性:封装;继承;多态。

  • 封装:将数据与操作数据的方法绑定起来,隐藏实现细节,对外提供接口。
    • ①通过私有化类的成员变量,通过公共的getter和setter方法来调用和修改;
    • ②还可以对类的其他结构进行“封装”;
    • ③权限修饰符:public、protected、private、缺省。
  • 继承:代码重用;可扩展性。
    • 通过让一个类A继承另一个类B,就可以获取类B中的结构。Java中的继承只支持单继承。
  • 多态:允许不同子类对象对同一消息做出不同响应。多态的三个必要条件:继承、方法的重写、父类引用指向子类对象。
    • ①体现:方法的重载和重写;子类对象的多态性;
    • ②子类对象多态性的使用:虚拟方法调用;
    • ③向上转型、向下转型(instanceof)

2)权限修饰符

修饰符 类内部 同一个包 子类 任何地方
private Yes      
default Yes Yes    
protected Yes Yes Yes  
public Yes Yes Yes Yes

class的权限修饰符只能是public或default。

3)可变个数的形参(任意多个参数)

    1. 格式:对于方法的形参:数据类型 ... 形参名
    1. 可变个数的形参的方法与同名的方法之间构成重载;
    1. 可变个数的形参在调用时,个数从0开数,到无穷多个都可以;
    1. 使用可变多个形参的方法与方法的形参使用数组是一致的;
    1. 若方法中存在可变个数的形参,那么一定要声明在方法形参的最后;
    1. 在一个方法中,最多声明一个可变个数的形参。

4)重写和重载

重载:

要求:1)同一个类中;2)方法名必须相同;3)方法的参数列表不同(个数、类型)

方法的重载与方法的返回值类型没有关系。

构造器是可以重载的。

重写:

  1. 前提:有子类继承父类。

  2. 子类继承父类以后,如果父类的方法对子类不适用,那么子类可以对父类方法重写。

  3. 重写规则:1)要求子类方法的“返回值类型 方法名(参数列表)”与父类方法一样;

    ​ 2)子类方法的修饰符不能小于父类方法;

    3)若父类方法抛异常,那么子类方法抛的异常类型不能大于父类的; 4)子父类的方法必须同为static或同为非static的。

5)多态

在Java中有两种体现:

  • 方法的重载和重写
  • 子类对象的多态性—–抽象类和接口

Java引用变量有两个类型:编译时类型运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。若编译时类型和运行时类型不一致,就出现多态(Polymorphism)。

根据对象对方法进行选择,称为分派:

  • 编译期的静态多分派:overloading重载,根据调用引用类型和方法参数决定调用哪个方法(编译器)。
  • 运行期的动态单分派:overriding 重写,根据指向对象的类型决定调用哪个方法(JVM)。

例:

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
class TestPolymorphism {
    @Test
    void test1() {
        // 子类对象的多态性:父类的引用指向子类对象
        // 程序运行分为编译状态和运行状态
        // 对于多态性来说:
        // 编译时,"看左边"(=号左边),将此引用变量理解为父类的类型;
        // 运行时,"看右边"(=号右边),关注真正对象的实体--子类的对象,那么执行的方法就是子类重写的。
        Person p1 = new Man(); // 向上转型(子类->父类)
        // 虚拟方法调用:通过父类的引用指向子类的对象实体,当调用方法时,实际执行的是子类重写父类的方法
        p1.eat();  // 调用Man类的eat()方法
        p1.walk(); // 调用Man类的walk()方法
        // p1.manF(); // 编译不通过,Person类没有Man类的新增方法,不能调用
        System.out.println(p1.id); // 打印Person类的id,类的属性无多态性。

        Person p2 = new Woman();
        p2.eat();
        p2.walk();
        Woman w = (Woman) p2; // 向下强制转型
        w.womanF(); // 强转后能调用子类方法

        // instanceof
        if (p1 instanceof Woman) {
            Woman w2 = (Woman) p1;
            w2.womanF();
        }

        Woman w1 = (Woman) p1; // 编译通过,执行报错,java.lang.ClassCastException类型强转错误
        w1.womanF();
    }
}

6)抽象类与接口

抽象类就是没有完全实现的类,部分实现在其子类中;接口和抽象类的主要区别是接口往往是更通用的一些功能,或者说接口比抽象类更抽象。

区别:

  • 1)抽象类中方法可以不是抽象的;接口中的方法必须是抽象方法;
  • 2)抽象类中可以有普通的成员变量;接口中的变量必须是 static final 类型的,必须被初始化 , 接口中只有常量,没有变量。
  • 3)抽象类只能单继承,接口可以继承多个父接口;
  • 4)Java8 中接口中会有 default 方法,即方法可以被实现。

使用场景:

  • 如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。
  • 如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。

7)内部类

  • 在另一个类的里面定义的类就是内部类
  • 内部类是编译器现象,与虚拟机无关。
  • 编译器会将内部类编译成用$分割外部类名和内部类名的常规类文件,而虚拟机对此一无所知。

内部类可以是static的,也可用public,default,protected和private修饰。 而外部类即类名和文件名相同的只能使用public和default。

优点:

  • 每个内部类都能独立地继承一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
  • 接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。

用内部类还能够为我们带来如下特性

  • 1、内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外部对象的信息相互独立。

  • 2、在单个外部类中,可以让多个内部类实现不同的接口,或者继承不同的类。外部类想要多继承的类可以分别由内部类继承,并进行Override或者直接复用。然后外部类通过创建内部类的对象来使用该内部对象的方法和成员,从而达到复用的目的,这样外部内就具有多个父类的所有特征。

  • 3、创建内部类对象的时刻并不依赖于外部类对象的创建。

  • 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。

  • 5、内部类提供了更好的封装,除了该外部类,其他类都不能访问

只有静态内部类可以同时拥有静态成员和非静态成员,其他内部类只有拥有非静态成员。

成员内部类

就像外部类的一个成员变量。

  • 注意内部类的对象总有一个外部类的引用
  • 当创建内部类对象时,会自动将外部类的this引用传递给当前的内部类的构造方法。

静态内部类

就像外部类的一个静态成员变量。

1
2
3
4
5
6
7
8
9
public class OuterClass {

    private static class StaticInnerClass {
        int id;
        static int increment = 1;
    }
}
//调用方式:
//外部类.内部类 instanceName = new 外部类.内部类();

局部内部类

定义在一个方法或者一个块作用域里面的类。

想创建一个类来辅助我们的解决方案,又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。

JDK1.8之前不能访问非final的局部变量!

生命周期不一致:

  • 方法在栈中,对象在堆中;方法执行完,对象并没有死亡
  • 如果可以使用方法的局部变量,如果方法执行完毕,就会访问一个不存在的内存区域。
  • 而final是常量,就可以将该常量的值复制一份,即使不存在也不影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
public Destination destination(String str) {
    class PDestination implements Destination {
        private String label;

        private PDestination(String whereTo) {
            label = whereTo;
        }
        public String readLabel() {
            return label;
        }
    }
    return new PDestination(str);
}

匿名内部类

必须继承一个父类或实现一个接口。

匿名内部类和局部内部类在JDK1.8 之前都不能访问一个非final的局部变量,只能访问final的局部变量,原因是生命周期不同,可能栈中的局部变量已经被销毁,而堆中的对象仍存活,此时会访问一个不存在的内存区域。假如是final的变量,那么编译时会将其拷贝一份,延长其生命周期。

拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。

但在JDK1.8之后可以访问一个非final的局部变量了,前提是非final的局部变量没有修改,表现得和final变量一样才可以!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface AnonymousInner {
    int add();
}
public class AnonymousOuter {
    public AnonymousInner getAnonymousInner(){
        int x = 100;
        return new AnonymousInner() {
            int y = 100;
            @Override
            public int add() {
                return x + y;
            }
        };
    }
}

8)关键字

(1)this

this表示当前对象,可以调用类的属性、方法和构造器。在方法内需要用到调用该方法的对象时,就用this。

  • 它在方法内部使用,即这个方法所属对象的引用;
  • 它在构造器内部使用,表示该构造器正在初始话的对象。

可以在构造器中通过this(形参)的方式显示的调用本类中其他重载的指定构造器。要求:

  • 在构造器内部必须声明在首行;
  • 若一个类中有n各构造器,那么最多有n-1个构造器中使用this(形参)

(2)super

调用父类的属性和方法,在子类构造方法中调用父类的构造器;

尤其当子父类出现同名成员时,可以用super进行区分;

super的追溯不仅局限于直接父类;

super和this的用法想象,this代表本类的对象引用,super代表父类的内存空间标识。

通过super(形参列表)显示调用父类中指定的构造器:

​ 1)在构造器内部,super(形参列表)b必须要声明在首行;

​ 2)在构造器内部,this(形参列表)super(形参列表)只能出现一个;

​ 3)当构造器中不显示调用this(形参列表)super(形参列表)其中任何一个,默认调用的是父类空参构造器。

设计一个类时尽量提供一个空参构造器!(继承、反射常用)。

根父类:Object。

(3)static(重要)

static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。

对于非静态数据成员,每个类对象都有自己的拷贝。而(static)静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问。也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新;静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义。

static修饰的成员会在构造方法之前执行,static成员不能调用非static的成员。

①修饰成员变量

静态成员变量(类变量)。

  1. 由类创建的所有对象,都共用这一属性;
  2. 当其中一个对象对此属性进行修改,会导致其他对象对此属性的一个调用。
  3. 类变量随着类的加载而加载的,而且独一份;
  4. 静态变量可以直接通过类.类变量的形式来调用;
  5. 类变量的加载早于对象;
  6. 类变量存在于静态域中。

静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

静态成员变量并发下不是线程安全的,并且对象是单例的情况下,非静态成员变量也不是线程安全的。

  • 怎么保证变量的线程安全:只有一个线程写,其他线程都是读的时候,加volatile;线程既读又写,可以考虑Atomic原子类和线程安全的集合类;或者考虑ThreadLocal

全局变量相比,使用静态数据成员有两个优势:

  • 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
  • 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能;
②修饰成员方法

静态成员方法(类方法)。

  1. 随着类的加载而加载,在内存中也是独一份;
  2. 可以直接通过类.类方法的方式进行调用;
  3. 出现在类体外的方法定义不能指定关键字static;
  4. 即使没有显式地声明为static,类的构造器实际上也是静态方法;
  5. 工具类多为静态方法;

静态结构(static、代码块、内部类)的比非静态的结构早加载,同时也比非静态结构回收晚

  • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员方法;
  • 非静态成员方法可以任意地访问静态成员函数和静态数据成员;
  • 静态成员方法不能访问非静态成员方法和非静态数据成员,也不能用this、super关键字;
③修饰代码块

静态代码块。

代码块(初始化块):类的成员(属性、方法、构造器、代码块、内部类)之一。

代码块如果有修饰符,只能是static。

非静态代码块:

  1. 可以对类的属性(静态的 & 非静态的)进行初始化操作,调用本类的方法(静态的 & 非静态的);
  2. 可以有一些输出语句;
  3. 一个类中可以有多个非静态代码块,多个代码块按顺序执行;
  4. 每创建一个对象,非静态代码块就加载一次;
  5. 非静态代码块的执行早于构造器;

静态代码块:

  1. 可以有一些输出语句;
  2. 随着类的加载而加载,而且只被加载一次;
  3. 多个静态代码块之间按照顺序执行;
  4. 静态代码块的执行早于非静态代码块;
  5. 静态代码块中只能执行静态的结构(类属性、类方法);

static用来构造静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 加载顺序测试

class Root {
    static {
        System.out.println("Root的静态初始化块");
    }

    {
        System.out.println("Root的普通初始化块");
    }

    Root() {
        System.out.println("Root的无参数构造器");
    }
}

class Mid extends Root {
    static {
        System.out.println("Mid的静态初始化块");
    }

    {
        System.out.println("Mid的普通初始化块");
    }

    private Mid() {
        System.out.println("Mid的无参数构造器");
    }

    Mid(String msg) {
        this();
        System.out.println("Mid的带参数构造器,参数值:" + msg);
    }
}

class Leaf extends Mid {
    static {
        System.out.println("Leaf的静态初始化块");
    }

    {
        System.out.println("Leaf的普通初始化块");
    }

    Leaf() {
        super("ssss");
        System.out.println("Leaf的无参数构造器");
    }
}

public class TestStatic {
    public static void main(String[] args) {
        new Leaf();
        System.out.println();
        new Leaf();
    }
}

/*输出结果:
Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块
Root的普通初始化块
Root的无参数构造器
Mid的普通初始化块
Mid的无参数构造器
Mid的带参数构造器,参数值:ssss
Leaf的普通初始化块
Leaf的无参数构造器

Root的普通初始化块
Root的无参数构造器
Mid的普通初始化块
Mid的无参数构造器
Mid的带参数构造器,参数值:ssss
Leaf的普通初始化块
Leaf的无参数构造器
*/
④修饰内部类

静态内部类。

  • 成员内部类和静态内部类的区别:
    • 1)前者只能拥有非静态成员;后者既可拥有静态成员,又可拥有非静态成员
    • 2)前者持有外部类的的引用,可以访问外部类的静态成员和非静态成员;后者不持有外部类的引用,只能访问外部类的静态成员
    • 3)前者不能脱离外部类而存在;后者可以
⑤修饰import

静态导包。

什么时候用static

需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。

static的用处比较多, 主要是用来编写不需要实例化就能使用的功能,典型应用是工具类,全局常量等,有时候也会利用它初始化顺序的特性。

单例模式

解决问题:使得y一个类只能够创建一个对象。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 饿汉式
class Signleton {
    // 1.私有化构造器,使得在类的外部不能调用该构造器
    private Signleton() {
    }

    // 2.在类的内部创建一个实例
    private static Signleton instance = new Signleton();

    // 3.私有化此对象,通过公共的方法来调用
    // 4.此公共的方法,只能通过类来调用,设置为static,同时类的实例也必须为static的
    public static Signleton getInstance() {
        return instance;
    }
}

// 饿汉式2
class Signleton3 {
    // 1.私有化构造器,使得在类的外部不能调用该构造器
    private Signleton3() {
    }

    // 2.在类的内部创建一个实例
    private static Signleton3 instance = null;

    // 静态代码块
    static {
        instance = new Signleton3();
    }

    // 3.私有化此对象,通过公共的方法来调用
    // 4.此公共的方法,只能通过类来调用,设置为static,同时类的实例也必须为static的
    public static Signleton3 getInstance() {
        return instance;
    }
}

// 懒汉式:可能存在线程安全问题
class Signleton2 {
    private Signleton2() {
    }

    private static Signleton2 instance = null;

    public static Signleton2 getInstance() {
        if (instance == null) {
            instance = new Signleton2();
        }
        return instance;
    }
}

public class TestSignle {
    public static void main(String[] args) {
        Signleton s1 = Signleton.getInstance();
        Signleton s2 = Signleton.getInstance();
        System.out.println(s1 == s2); //True

        Signleton2 s3 = Signleton2.getInstance();
        Signleton2 s4 = Signleton2.getInstance();
        System.out.println(s3 == s4); //True
    }
}
main()方法

public static void main(Stringp[] args)

由于Java虚拟机需要调用类的main()方法,所以该方法必须是public的,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的。

(4)final

final修饰一个类、方法、变量,表示”最终“,不可以被修改。

  • 类的不可修改指的是不可被继承,例如:String类、System类、StringBuffer类;

  • 方法则是不能被重写,例如:Object类中的getClass();

  • 变量就是不可重新赋值(即不可修改引用),必需在声明的同时、构造方法或代码块中显示的赋值,然后才能使用。

变量用static final修饰:全局常量,比如Math类的PI。

(5)abstract

抽象的,可以用来修饰类、方法。

abstract修饰类:抽象类

  1. 不可被实例化;
  2. 有构造器(凡是类都有构造器);
  3. 抽象方法所在的类,一定是抽象类;
  4. 抽象类中可以没有抽象方法;

abstract修饰方法:抽象方法

  1. 格式:public abstract void func();
  2. 抽象方法只保留方法的功能定义,具体的实现,交给继承抽象类的子类;
  3. 若子类继承抽象类,并重写了所有的抽象方法,则此类是一个”实体类“,可以实例化;
  4. 若子类继承抽象类,没有重写所有的抽象方法,意味着此类中仍有抽象方法,则此类必须用abstract修饰;

不能用abstract修饰属性、构造器(构造器不能被重写)、private(子类不能重写private方法)、static(通过类调未实现方法无意义)、final(final不能被重写)。

模板方法设计模式(TemplateMethod)

一部分确定,一部分不确定,把不确定的声明为abstract暴露给子类来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
abstract class Template {
    abstract void code();

    public void spendTime() {
        long start = System.currentTimeMillis();
        code();
        long end = System.currentTimeMillis();
        System.out.println("花费时间:" + (end - start));
    }
}

class SubTemplate extends Template {
    public void code() {
        for (int i = 2; i <= 1000000; i++) {
            int j = i;
        }
    }
}

public class TestTemplateMethod {
    public static void main(String[] args) {
        new SubTemplate().spendTime();
    }
}

(6)interface

接口是抽象方法和常量值的定义的集合,是与类并行的一个概念。从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。一个类可以实现多个接口,接口也可以继承其他接口。有了接口,就可以达到多重继承的效果。

  1. 特殊的抽象类,常量(public static final修饰)与抽象方法(public abstract修饰)的集合;
  2. 没有构造器;
  3. 接口定义的是一种功能,此功能可以被类实现(implements);
  4. 实现接口的类,必须要重写其中的所有抽象方法,才能实例化,若没有重写所有的抽象方法,此类仍为一个抽象类;
  5. 类可以实现多个接口(继承是单继承的);
  6. 接口之间可以继承,而且可以多继承;
  7. 接口与具体的实现类之间存在多态性;

接口用法:

  • 通过接口可以实现不相关类的相同行为,而不需要考虑这些类之间的层次关系;
  • 通过接口可以指明多个需要实现的方法,一般用于定义对象的扩张功能;
  • 接口主要用来定义规范,接触耦合关系。
工厂方法(FactoryMethod)

概述:定义一个用于创建对象的接口,让子类决定实例化哪一个类,FactoryMethod使一个类的实例化延迟到其子类。

适用性:

  1. 当一个类不知道它所必须创建的对象的类的时候;
  2. 当一个类希望由它的子类来指定它所创建的对象的时候;
  3. 当类将创建对象的职责委托给多个帮助子类中的某一个,并且希望将哪一个帮助子类是代理者这一信息局部化的时候。
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
36
37
38
39
40
41
42
43
44
45
46
47
interface IWorkFactory {
    Work getWork();
}

class StudentWorkFactory implements IWorkFactory {
    @Override
    public Work getWork() {
        return new StudentWork();
    }
}

class TeacherWorkFactory implements IWorkFactory {
    @Override
    public Work getWork() {
        return new TeacherWork();
    }
}

interface Work {
    void doWork();
}

class StudentWork implements Work {
    @Override
    public void doWork() {
        System.out.println("学生写作业");
    }
}

class TeacherWork implements Work {
    @Override
    public void doWork() {
        System.out.println("老师改作业");
    }
}

public class TestFactoryMethod {
    public static void main(String[] args) {
        IWorkFactory i = new StudentWorkFactory();
        i.getWork().doWork();

        IWorkFactory i1 = new TeacherWorkFactory();
        i1.getWork().doWork();
        // 编译的时候都是接口之间在操作,运行的时候是真正的对象在运作。
    }
}
代理模式(Proxy)

为其他对象提供一种代理以控制对这个对象的访问。

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
36
37
38
39
40
41
42
// 静态代理

interface MyObject {
    void action();
}

// 被代理类
class ObjectImpl implements MyObject {
    @Override
    public void action() {
        System.out.println("被代理类具体操作");
    }
}

// 代理类
class ProxyObject implements MyObject {
    MyObject obj;

    public ProxyObject() {
        System.out.println("代理类创建成功");
        obj = new ObjectImpl();
    }

    public void action() {
        System.out.println("代理类开始执行");
        obj.action();
        System.out.println("代理类执行结束");
    }
}

public class TestProxy {
    public static void main(String[] args) {
        MyObject obj = new ProxyObject();
        obj.action();
    }
}
/*输出结果
代理类创建成功
代理类开始执行
被代理类具体操作
代理类执行结束
*/

接口的应用:JDBC,不同的数据库不同的driver。

3.String

references

1)概览

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)

在 Java 8 中,String 内部使用 char 数组存储数据。

1
2
3
4
5
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

1
2
3
4
5
6
7
8
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

equals&hashCode

  • String重写了Object的hashCode和equals。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;

2)不可变的好处

1. 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

3. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。

4. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

Program Creek : Why String is immutable in Java?

3)String, StringBuffer and StringBuilder

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

StackOverflow : String, StringBuffer, and StringBuilder

4)String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。

1
2
3
4
5
6
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4);           // true

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

1
2
3
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6);  // true

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

5)new String(“abc”)

使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。

  • “abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
  • 而使用 new 的方式会在堆中创建一个字符串对象。

创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。

1
2
3
4
5
public class NewStringTest {
    public static void main(String[] args) {
        String s = new String("abc");
    }
}

使用 javap -verbose 进行反编译,得到以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ...
Constant pool:
// ...
   #2 = Class              #18            // java/lang/String
   #3 = String             #19            // abc
// ...
  #18 = Utf8               java/lang/String
  #19 = Utf8               abc
// ...

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String abc
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
// ...

在 Constant Pool 中,#19 存储这字符串字面量 “abc”,#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。

以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。

1
2
3
4
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

4.异常

Java异常体系

img

Java程序在执行过程中所发生的异常事件可分为两类:

  • Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
  • Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。如:空指针访问、试图读取不存在的文件、网络连接中断。 Exception这种异常又分为两类:运行时异常和编译异常。

Exception这种异常又分为两类:编译时异常和运行时异常

1、编译时异常(可查异常):在编译期间会出现的异常(执行javac命令时)。包含Exception中除RuntimeException及其子类之外的异常。如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。

2、运行时异常(不可查异常):在运行期间出现的异常(执行java命令时)。包含RuntimeException类及其子类,表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。

可查异常与不可查异常:

1、可查异常:编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除RuntimeException及其子类外,其他的Exception异常都属于可查异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用throws语句抛出,否则编译不通过。

2、不可查异常:编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。

异常处理

Java提供的是异常处理的抓抛模型

Java程序的执行过程中如果出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。异常对象的生成:由虚拟机自动生成或者由开发人员手动创建。

1、“抓”:抓住上一步抛出来的异常类的对象。如何抓?即为异常处理的方式,Java提供了两种方式来处理一个异常类的对象。

  • try_catch_finally
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try{
    // try内的变量类似局部变量,出了try{}语句就不能被调用
} catch (IOException e){
    // catch语句内部是对异常对象的处理:getMessage();printStackTrace();
} catch (Exception e){
    // 可以有多个catch语句,try中抛出的异常类对象从上往下去匹配catch中的异常的类型,
    // 一旦满足就执行catch中的代码,执行完,就跳过其后多余的catch语句。
    // 若catch中多个异常类型有包含关系,先小后大。
} finally{
    // 可选的,一定会被执行的代码。
    // 不管try、catch中是否仍有异常未被处理,不管是否有return语句。
}
// try_catch是可以相互嵌套的。

// 如果异常处理了,那么其后的代码继续正常运行。
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
// finally执行顺序
public class TestFinally {
    public static int method1() {
        try {
            System.out.println(10 / 0);
            return 1;
        } catch (Exception e) {
            e.printStackTrace();
            return 3;
        } finally {
            return 2;
        }
    }

    public static int method2() {
        try {
            return 1;
        } catch (Exception e) {
            e.printStackTrace();
            return 3;
        } finally {
            return 2;
        }
    }

    public static void main(String[] args) {
        int i = method1();
        System.out.println(i); // 2
        int i2 = method2();
        System.out.println(i2); // 2
    }
}
  • throws + 异常类型

public void method() throws IOException{}

2、“抛”:如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。当执行代码时,一旦出现异常,就会在异常的代码处生成一个对应的异常类型的对象,并将此对象抛出。

  • 一旦抛出此异常类的对象,那么程序就终止执行。
  • 此异常类的对象抛给方法的调用者。

自动抛出或手动抛出(throw + 异常类的对象【现成的或自己创建的异常类】)。

注意:throws和throw的区别。

自定义异常类:

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
// 1.必须在Throwable体系下,继承一个现有的异常类
// 2.提供一个序列号,提供几个重载的构造器
public class MyException extends RuntimeException{
    static final long serialVersionUID = -703489238992389L;
    public MyException(){
    }
    public MyException(String msg){
        super(msg);
    }
}

// 使用自定义异常
public int compareTo(Object obj) throws Exception{
    if(this == obj){
        return 0;
    }
    else if(obj instanceof MyObject){
        MyObject myObj = (MyObject)obj;
        if(this.field > myObj.field){
            return 1;
        }else if(this.field == myObj.field){
            return 0;
        }else{
            return -1;
        }
    }else{ // 不满足所有条件
        throw new MyException("传入的类型有误!");
    }
}

抛异常的方法的重写规则:子类重写的父类的方法,其抛出的异常类型只能是被重写的方法的异常类的子类或一样。

5个关键字搞定异常处理:try、catch、finally、throw、throws。

5.集合、泛型

Java集合框架结构:

img

  • Iterator迭代器接口

  • Collection接口:
    • List:元素有序,可重复。
    • Set:元素无序,不可重复。
  • Map接口:具有映射关系“K-V对”的集合。
  • Collections工具类

Java集合分为Collection和Map两种体系。

1)Collection

判断对象是否相同依据:根据equals()方法判断,自定义类需重写equals()方法

常用方法:size(),add(),addAll(),isEmpty(),clearI(),toArray(),iterator()

使用equals()的:contains(),containsAll(),retainAll(),remove(),removeAll(),equals(),hashCode()

2)Iterator

用来遍历集合Collection元素。

hasNext(),next()

3) List

List中相对于Collection,新增了一些根据索引进行操作的方法:

  • void add(int index, E element)
  • boolean addAll(int index, Collection<? extends E> c)
  • E get(int index)
  • int indexOf(Object o)
  • int lastIndexOf(Object o)
  • E remove(int index)
  • E set(int index, E element)
  • List **subList**(int fromIndex, int toIndex)

增删改查:add、remove、set、get。

ArrayList

底层存储:数组。

LinkedList

底层存储:链表。

Vector

古老的实现类,JDK1.0。线程安全,效率低。

4)Set

HashSet

存储的元素是无序的,不可重复的!

  • 无序性 != 随机性。真正的无序性,指的是元素在底层存储的位置是无序的。
  • 不可重复性:当向set中添加相同的元素时,不能添加进去。

要求添加进set元素中的所有类,一定要重写equals()方法和hashCode()方法,进而保证set中元素的不可重复性。

set中的元素如何存储?

先比较hashCode()值,再比较equals()。

使用哈希算法。当向set中添加对象时,首先调用此对象的hashCode()方法,计算此对象的哈希值,此哈希值决定了此对象在set中的存储位置。若此位置之前没有对象存储,则这个对象直接存储到此位置。若此位置已有对象存储,再通过equals()方法比较两个对象是否相同。如果相同,后一个对象就不能再添加进来。

如果不同呢?则存储。(不建议如此)

要求:hashCode()方法和equals()方法一致。尽量设计一个好的hashCode()方法。

LinkedHashSet

使用链表维护了一个添加进集合的顺序。遍历时是添加进去的顺序。

TreeSet

红黑树结构。

  • 自动初始化泛型。
  • 按照添加进集合中的元素的指定顺序遍历。像String,包装类等默认按照从小到大的顺序。

自定义类排序:

  • 自然排序(可以修改要排序的类):要实现Comparable接口,并重写compareTo(Object obj)抽象方法。按照compareTo()方法进行比较,一旦返回0,即使两个对象不同,但TreeSet认为两个对象相同,不添加进集合。所以compareTo()与hashCode()方法和equals()方法三者应保持一致。
  • 定制排序(不能修改要排序的类):创建一个实现了Comparator接口类的对象,并重写compare(Object o1,Object o2)方法;将此对象作为形参传递给TreeSet的构造器中;同样compare()方法与hashCode()方法和equals()方法三者应保持一致。

5.)Map

  • Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value。
  • Map中的key和value都可以是任何引用类型的数据。
  • Map中的key用Set来存放,不允许重复,即同一个Map对象所对应的类,须重写hashCode()和equals()方法。
  • Map中的Value使用Collection存放的。

常用方法:

​ 添加删除:put(),putAll(),remove(),clear()

​ 元视图操作:keySet(),values(),entrySet()

​ 元素查询:get(),containsKey(),containsValue(),size(),isEmpty,euqals()

HashMap

HashMap判断两个key相等的标准:两个key通过equals()方法返回true,hashCode值也相等。

HashMap判断两个value相等的标准:两个value通过equals()方法返回true。

HashSet是HashMap的特殊实现!

LinkedHashMap

用一个列表维护添加进的顺序。

TreeMap

按照添加进Map中的元素的key的指定属性进行排序。要求:key必须是同一个类的对象!

针对key:自然排序 vs 定制排序。

Hashtable

古老的Map实现类,JDK1.0。线程安全。

与HashMap不同,Hashtable不允许使用null作为key和value。

Properties

常用来处理属性文件。键和值都为String类型的。

1
2
3
4
5
6
7
// jdbc.properties:
// user=root
// password=123456

Properties pros = new Properties()
pros.load(new FileInputStream(new File("jdbc.properties")));
String user = pros.getProperty("user");

6)Collections

Collections是一个操作集合的工具类。提供了一些列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

排序操作:(均为static方法)

  • reverse(List):反转List
  • shuffle(List):随机排序
  • sort(List):升序排序
  • sort(List,Comparator):根据Comparator排序
  • swap(List,int,int):交换元素

查找、替换:

  • Object max(Collection)
  • Object max(Collection,Comparator)
  • Object min(Collection)
  • Object min(Collection,Comparator)
  • int frequenc(Collection,Object):出现次数
  • void copy(List dest,List src):复制
  • boolean replaceAll(List list,Object oldVal,Object newVal):替换

同步控制:

Collections类中提供了多个synchronizedXxx()方法,可以将指定集合包装成线程同步安全的集合。

  • static Collection **synchronizedCollection**(Collection c)
  • static List **synchronizedList**(List list)
  • static Map **synchronizedMap**(Map m)
  • static Set **synchronizedSet**(Set s)

7)泛型

为什么要有泛型(Generic)?

  • 解决元素存储的安全性问题(不使用泛型,任何Object及其子类对象都可以添加进集合);
  • 解决获取元素时,需要类型强转的问题。

把一个集合中的内容限制为一个特定的数据类型是泛型背后的核心思想。

注意:

  • 静态方法中不能使用泛型类(实例化时才确定泛型类型,实例化之前static就要加载);
  • 如果泛型类是一个接口或抽象类,则不可以创建泛型类的对象;
  • 不能在catch中使用泛型;
  • 从泛型类派生子类,泛型类型需具体化;

自定义泛型类、泛型接口、泛型方法

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Order<T>{
    private String orderName;
    private int orderId;
    private T t;
    List<T> list = new ArrayList<>();
    
    public void add(){
        list.add(t);
    }
    public T getT(){
        return t;
    }
    public void setT(T t){
        this.t = t;
    }
    // 泛型方法
    public <E> E getE(E e){
        return e;
    }
    // 泛型方法:数组到集合的复制
    public <E> List<E> fromArrayToList(E[] e,List<E> list){
        for(E e1:e){
            list.add(e1);
        }
        return list;
    }
    
    // get、set
    // toString()
}
public class SubOrder<T> extends Order<T>{
    // ...
}
public class SubOrder2<Integer> extends Order<Integer>{
    // ...
}

public class TestGeneric{
    @Test
    public void test(){
        Order<Boolean> order = new Order<Boolean>();
        order.setT(true);
        order.add();
        System.out.println(order.list);
        
        // 泛型方法调用:指明泛型方法类型
        Integer i = order.getE(18);
        Double d = order.getE(1.8);
        
        Integer[] in = new Integer{1,2,3};
        List<Integer> list1 = new ArrayList<Integer>();
        order.fromArrayToList(in,list1);
    }
}

泛型与继承的关系

若类A是类B的子类,List不是List的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestGeneric{
    @Test
    public void test(){
        Object obj = null;
        String str = "aa";
        obj = str;
        
        List<Object> list = null;
        List<String> list1 = new ArrayList<String>();
        list = list1; // 错误
        // 假设list = list1满足,指向同一个堆空间
        // list.add(123);
        // String str = list1.get(0); // 出现问题:指定String,返回int?所以假设不成立。
    }
}

通配符

List、List……都是List<?>的子类。

? extends A:可以存放A及其子类;

? super A:可以存放A及其父类。

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
public class TestGeneric{
	@Test
    void test2() {
        List<?> list = null;
        List<Object> list1 = new ArrayList<Object>();
        List<String> list2 = new ArrayList<String>();
        list = list1; // 没问题
        list = list2; // ok

        show1(list1);
        // show1(list2); // 不可以
        show2(list1);
        show2(list2);

        List<? extends Number> list3 = null;
        List<Integer> list4 = null;
        list3 = list4;
        // list3 = list1; // no

        // 不允许向声明为通配符的集合类中写入对象。唯一例外的是null。
        List<?> list5 = null;
        // list5.add("cc"); // 不可以
        // list5.add(124); // 不可以
        list5.add(null); // ok,运行报错java.lang.NullPointerException
    }

    void show1(List<Object> list) {
        // ...
    }

    void show2(List<?> list) {
        // ...
    }
}

6.IO

IO流:InputOutput

按操作方式分类结构图

img

按操作对象分类结构图

img

IO:

  • java.io.File类
  • 文件流(节点流):FileInputStream/FileOutputStream;FileReader/FileWriter
  • 缓冲流:BufferedInputStream/BufferedOutputStream;BufferedReader/BufferedWriter
  • 对象流:ObjectInputStream/ObjectOutputStream
  • 随机存取文件流:RandomAccessFile
  • 转换流:InputStreamReader/OutputStreamWriter
  • 标准输入/输出流
  • 打印流:PrintStream/PrintWriter
  • 数据流:DataInputStream/DataOutputStream

1)File类

  • java.io.File类:文件和目录路径名的抽象表示形式,与平台无关

  • File能新建、删除、重命名文件和目录,但File不能访问文件内容本身,访问需要输入/输出流。-

  • File对象可以作为参数传递给流的构造函数。

2)IO流分类

img

Java IO体系总结:

  1. IO流的分类:

    • 按照流的流向分,可以分为输入流和输出流;
    • 按照操作单元划分,可以划分为字节流字符流(文本文件txt,word文件不能用);
    • 按照流的角色划分为节点流(直接作用在文件上的)和处理流
  2. 流的原理浅析:

    java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。

    • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
    • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

Java 的 I/O 大概可以分成以下几类:

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 网络操作:Socket
  • 新的输入/输出:NIO

IO流使用:

1
2
3
4
5
6
7
8
9
10
11
public void testIO(){
    // 1.创建一个File对象
    // 2.创建一个流对象,将File对象作为形参传递。
    try{
        // 3.操作
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        // 4.关闭流
    }
}

缓冲流比文件流快:文件流是阻塞式的,缓冲流非阻塞。底层使用数组实现的。

转换流:提供了在字节流和字符流之间的转换。当字节流中的数据都是字符时,转成字符流操作更高效。

  • InputStreamReader:字节流->字符流,解码。(前提是文本文件)

  • OutputStreamWriter:字符流->字节流,编码。

标准输入/输出流:System.in/System.out。

打印流:字节流-PrintStream,字符流:PrintWriter。

数据流:用来处理基本数据类型、String、字节数组的数据。DataInputStream、DataOutputStream。

对象流:把对象写入数据源。ObjectInputStream、ObjectOutputStream。

对象的序列化:允许把内存中的Java对象转换成平台无关的二进制流,从而实现磁盘保存或网络传输。实现Serializable接口的对象都可以转换为字节数据。

要实现序列化的类:

  • 要求此类是可序列化的,实现Serializable接口。
  • 要求类的属性同样要实现Serializable接口。
  • 凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态变量。public static final long serialVersionUID;用来表明类的不同版本间的兼容性。如果没有定义,Java内部自动生成,如果类的源码修改,值也修改,故建议显示声明。
  • 不能序列化static和transient修饰的成员变量。

随机存取文件流:RandomAccessFile类支持“随机访问”的方式,可以直接跳到文件的任意地方来读、写文件。

指定mode参数:r、rw、rwd、rws。定位方法:getFilePointer()、seek()。

Java IO:体现了装饰者设计模式

1574771158038

1574771225334

图源:尚硅谷-saprk

7.并发编程

https://github.com/CyC2018/CS-Notes/blob/master/notes/Java 并发

程序、进程、线程的概念:

  • 程序:静态的代码;
  • 进程:执行中的程序;
  • 线程:进程的进一步细分,程序的一条执行路径。

1)线程

  • 线程的状态
  • 线程的几种实现方式
  • 三个线程轮流打印ABC十次
  • 判断线程是否销毁
  • yield功能
  • 给定三个线程t1,t2,t3,如何保证他们依次执行

线程实现方式

  • ①继承 Thread 类 ```java // 创建一个子线程,完成1-100的打印输出。同样的,主线程执行同样的操作。

// 1.创建一个继承于Thread的子类 class SubThread extends Thread{ // 2.重写Thread类的run()方法,方法内实现此子线程要完成的功能 public void run(){ for(int i=1;i<=100;i++){ System.out.println(Thread.currentThread().getName() + “:” + i); } } }

public class TestThread{ public static void main(String[] args){ // 3.创建一个子类的对象 SubThread st = new SubThread(); // 4.调用线程的start()方法,启动此线程,调用run()执行。 st.start(); // 5.一个线程只能够执行一次start()方法。 // st.start(); // java.lang.IllegalThreadStateException st.run(); // 不行,只是调用方法而已。

1
2
3
4
    for(int i=1;i<=100;i++){
        System.out.println(Thread.currentThread().getName() + ":" + i);       
    }
} }

// 输出:主线程和子线程交替打印输出。

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
36
37
38
**Thread的常用方法:**

1. start():启动线程并执行相应的run()方法;
2. run():子线程要执行的代码放入run()方法中;
3. currentThread():静态的,调取当前的线程;
4. getName():获取此线程的名字;
5. setName():设置此线程的名字;
6. yield():释放当前CPU的执行权;
7. join():在A线程中调用B线程的join()方法,表示当执行到此方法,A线程停止执行,直至B线程执行完毕,A线程再接着join()之后执行;
8. isAlive():判断当前线程是否还存活;
9. sleep(long s):显示的让当前线程睡眠s毫秒,需要try_catch;
10. 线程通信:wait()、notify()、notifyAll();
11. 设置线程的优先级:范围【1-10】,默认5。getPriority():返回线程优先值;setPriority(int newPriority):设置线程的优先级;


- ②实现 Runnable 接口
```java
// 1.创建一个实现了Runnable接口的类
class PrintNum1 implements Runnable{
    // 2.实现接口的抽象方法
    public void run(){
        for(int i=1;i<=100;i++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }    
}

public class TestThread{
    public static void main(String[] args){
        // 3.创建一个Runnable接口实现类的对象
		PrintNum1 p = new PrintNum1();
        // 4.将此对象作为形参传递给Thread类的构造器,创建Thread类的对象,此对象即为一个线程。
        Thread t1 = new Thread(p);
        // 5.调用start()方法,启动线程并执行run()方法。
        t1.start();
    }
}

继承Thread类 vs 实现Runnable接口:

  1. 联系:public class Thread implements Runnable,Thread实现了Runnable接口;
  2. 哪个方式好?实现Runnable接口的方式优于继承Thread的方式:
    • 避免了Java单继承的局限性;
    • 如果多个线程要操作同一份资源(或数据),实现Runnable接口更好;
    • 类可能只要求可执行就行,继承整个 Thread 类开销过大。
  • ③实现 Callable 接口

Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

线程生命周期

img

  • 新建:Thread类或其子类的对象被声明并创建时,新生成的线程对象处于新建状态。
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件。
  • 运行:当就绪的线程被调用并获得CPU资源时,便进入运行状态,run()方法定义了线程的功能。
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止。

2)线程同步

线程安全问题存在的原因:由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。

解决线程安全问题:必须让一个线程操作共享数据完毕后,其它线程才有机会参与共享数据的操作。

Java如何实现线程的安全:线程的同步机制。

同步代码块

1
2
3
4
5
public void func() {
    synchronized (this) { // 同步监视器
        // 需要被同步的代码块(即为操作共享数据的代码)
    }
}
  1. 共享数据:多个线程共同操作的同一个数据(变量);
  2. 同步监视器:由一个类的对象(任何类都行,现成的:this,但不是任何情况都能用this)来充当。哪个线程获取此监视器,谁就执行被同步的代码。锁。它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
  3. 所有的线程必须共用同一把锁。

对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SynchronizedExample {
    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}
// 输出:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}
// 输出:0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

同步方法

保证一个线程执行此方法时,其它线程在外等待直至此线程执行完此方法。

1
2
3
public synchronized void func () {
    // ...
}

它和同步代码块一样,作用于同一个对象。

同步一个类

1
2
3
4
5
public void func() {
    synchronized (SynchronizedExample.class) {
        // ...
    }
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}
// 输出:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

同步一个静态方法

1
2
3
public synchronized static void fun() {
    // ...
}

作用于整个类。

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LockExample {
    private Lock lock = new ReentrantLock();
    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁,从而避免发生死锁。
        }
    }
}
public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}
// 输出:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

synchronized和ReentrantLock比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock 可中断,而 synchronized 不行。

4. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象。

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

单例模式之懒汉模式

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
// 懒汉式:可能存在线程安全问题
class Signleton2{
    // 1.
    private Signleton2(){}
    // 2.
    private static Signleton2 instance = null;
    // 3.
    public static Signleton2 getInstance(){
        if(instance == null){ // 多个判断,使效率略高一点
            synchronized(Signleton2.class){ // 使用当前类本身
                if(instance == null){
                    instance = new Signleton();
                }
            }
        }
        return instance;
    }
}

public class TestSignle{
    public static void main(String[] args){
        Signleton2 s3 = Signleton2.getInstance();
        Signleton2 s4 = Signleton2.getInstance();
        System.out.println(s3 == s4); //True
    }
}

线程同步的弊端:由于同一个时间只能有一个线程访问共享数据,效率变低。

3)线程通信

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

join()

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。

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
36
public class JoinExample {
    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }
    private class B extends Thread {
        private A a;
        B(A a) {
            this.a = a;
        }
        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }
    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }
}
public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
}
// 输出:
// A
// B

wait() notify() notifyAll()

  • wait():令当前线程挂起并释放CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问。

  • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
  • notifyAll():唤醒正在排队等待资源的所有线程结束等待。

它们都属于 Object 的一部分,而不属于 Thread。只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WaitNotifyExample {
    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }
    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
// 输出:
// before
// after

wait() 和 sleep() 的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

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
public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
// 输出:
// before
// after

4)J.U.C - AQS

java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。

CountDownLatch

用来控制一个或者多个线程等待多个线程。

维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CountdownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}
// run..run..run..run..run..run..run..run..run..run..end

CyclicBarrier

用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。

和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。

CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。

CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。

1
2
3
4
5
6
7
8
9
10
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CyclicBarrierExample {
    public static void main(String[] args) {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.print("after..");
            });
        }
        executorService.shutdown();
    }
}
// before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..

Semaphore

Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SemaphoreExample {
    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.print(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}
// 2 1 2 2 2 2 2 1 2 2

FutureTask

在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。

1
2
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>

FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。

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
public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 0; i < 100; i++) {
                    Thread.sleep(10);
                    result += i;
                }
                return result;
            }
        });

        Thread computeThread = new Thread(futureTask);
        computeThread.start();

        Thread otherThread = new Thread(() -> {
            System.out.println("other task is running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        otherThread.start();
        System.out.println(futureTask.get());
    }
}
// other task is running...
// 4950

BlockingQueue

java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:

  • FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
  • 优先级队列 :PriorityBlockingQueue

提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。

使用 BlockingQueue 实现生产者消费者问题

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
36
37
38
39
40
41
42
43
44
public class ProducerConsumer {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

    private static class Producer extends Thread {
        @Override
        public void run() {
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("produce..");
        }
    }

    private static class Consumer extends Thread {

        @Override
        public void run() {
            try {
                String product = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.print("consume..");
        }
    }
}
public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
        Producer producer = new Producer();
        producer.start();
    }
    for (int i = 0; i < 5; i++) {
        Consumer consumer = new Consumer();
        consumer.start();
    }
    for (int i = 0; i < 3; i++) {
        Producer producer = new Producer();
        producer.start();
    }
}
// produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..

ForkJoin

主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。

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
36
37
public class ForkJoinExample extends RecursiveTask<Integer> {

    private final int threshold = 5;
    private int first;
    private int last;

    public ForkJoinExample(int first, int last) {
        this.first = first;
        this.last = last;
    }

    @Override
    protected Integer compute() {
        int result = 0;
        if (last - first <= threshold) {
            // 任务足够小则直接计算
            for (int i = first; i <= last; i++) {
                result += i;
            }
        } else {
            // 拆分成小任务
            int middle = first + (last - first) / 2;
            ForkJoinExample leftTask = new ForkJoinExample(first, middle);
            ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
            leftTask.fork();
            rightTask.fork();
            result = leftTask.join() + rightTask.join();
        }
        return result;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    ForkJoinExample example = new ForkJoinExample(1, 10000);
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    Future result = forkJoinPool.submit(example);
    System.out.println(result.get());
}

ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数。

1
public class ForkJoinPool extends AbstractExecutorService

ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。

img

8.注解、反射

1)注解(Annotation)

什么是注解:

从本质上来说,注解是一种标签,其实质上可以视为一种特殊的注释,如果没有解析它的代码,它并不比普通注释强。

解析一个注解往往有两种形式:

  • 编译期直接的扫描:编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。这种情况只适用于 JDK 内置的注解类。
  • 运行期的反射:如果要自定义注解,Java 编译器无法识别并处理这个注解,它只能根据该注解的作用范围来选择是否编译进字节码文件。如果要处理注解,必须利用反射技术,识别该注解以及它所携带的信息,然后做相应的处理。

(1)元注解

JDK的元Annotation用于修饰其他Annotation定义。对注解进行注解。

@Retention:指定注解可以保留多长时间【SOURCE、CLASS、RUNTIME】。

@Target:指定注解能用于修饰哪些程序元素。

@Documented:指定注解被javadoc工具提取成文档。

@Inherited:指定修饰的注解具有继承性。

(2)基本注解类型

@Override:限定重写父类方法,该注解只能用于方法

@Deprecated:用于表示某个程序元素(类、方法等)已过时;

@SuppressWarnings:抑制编译器警告

(2)自定义注解

1
2
3
public @interface MyAnnotation{
    String value() default "hello";
}

2)反射

反射(Reflection)是被视为动态语言的关键,反射机制允许程序在执行期借助于 Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

反射的主要应用场景有:

  • 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
  • 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。
  • 注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。
  • 可扩展性功能 - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。

Java反射机制提供的功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的成员变量和方法
  • 生成动态代理

反射相关的主要API:

  • java.lang.Class:代表一个类
  • java.lang.reflect.Method:代表类的方法
  • java.lang.reflect.Field:代表类的成员变量
  • java.lang.reflect.Constructor:代表类的构造方法
  • … …
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
class TestReflection {
    @Test
    void test1() throws IllegalAccessException, InstantiationException,
            NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
        Class clazz = Person.class;
        // 1.创建clazz对应的运行类Person类的对象
        Person p = (Person) clazz.newInstance();
        System.out.println(p);
        // 2.通过反射调用运行时类的指定的属性
        // public
        Field f1 = clazz.getField("name");
        f1.set(p, "ssss");
        // private
        Field f2 = clazz.getDeclaredField("age");
        f2.setAccessible(true);
        f2.set(p, 10);
        // 3.通过反射调用运行时类的指定的方法
        // public
        Method m1 = clazz.getMethod("hello");
        m1.invoke(p);
        // private
        Method m2 = clazz.getDeclaredMethod("display", String.class);
        m2.setAccessible(true);
        m2.invoke(p, "CHN");
    }
}

(1)Class类

java.lang.Class:反射的源头。

》正常方式:引入需要的“包类”名称 -> 通过new实例化 -> 取得实例化对象

》反射方式:实例化对象 -> getClass()方法 -> 得到完整的”包类”名称

对于每个类而言,JRE都为其保留一个不变的Class类型的对象,一个Class对象包含了特定某个类的有关信息。

  • Class本身也是一个类
  • Class对象只能由系统建立
  • 一个类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个Class实例所生成
  • 通过Class可以完整地得到一个类中的完整结构(属性、方法、构造器、父类、所在的包、异常、注解…)
  • 反射的应用:注解、动态代理、开发通用框架

获取Class类的实例的4种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestClass {
    @Test
    void test() throws Exception {
        // 1.调用运行时类本身的.class属性
        Class clazz1 = Person.class;
        Class clazz2 = String.class;
        // 2.通过运行时类的对象获取
        Person p = new Person();
        Class clazz3 = p.getClass();
        // 3.通过Class的静态方法获取
        String className = "stone.learn.java.lang.reflect.Person";
        Class clazz4 = Class.forName(className);
        // 4.通过类加载器
        ClassLoader classLoader = this.getClass().getClassLoader();
        Class clazz5 = classLoader.loadClass(className);

        System.out.println(clazz1 == clazz2); // false
        // 一个类在JVM中只会有一个Class实例
        System.out.println(clazz1 == clazz3); // true
        System.out.println(clazz1 == clazz4); // true
        System.out.println(clazz1 == clazz5); // true
    }
}

有了Class对象,能做什么?

  1. 创建类的对象:调用Class对象的newInstance()方法。要求:①类必须有一个无参的构造器;②类的构造器的访问权限要足够。[有参:需要用getDeclaredConstructor(Class … parameterTypes)方法]
  2. 通过反射获取类的完整结构:Field、Method、Constructor、Superclass、Interface、Annotation…
  • Field

》getFields():获取运行时类中及其父类声明为public的属性;

》getDeclaredFields():获取运行时类本身所有的属性;

》field.getModifiers():获取属性的权限修饰符,返回int型;

》Modifier.toString(field.getModifiers()):获取属性的权限修饰符,返回string;

》field.getType():获取属性的类型;

  • Method

》getMethods():获取运行时类及其父类声明为public的方法;

》getDeclaredMethods():获取运行时类本身所有的方法;

》method.getAnnotations():获取方法的注解;

》method.getModifiers():获取方法的权限修饰符,返回int型;

》method.getReturnType():获取方法返回值类型;

》method.getName():获取方法名;

》method.getParameterTypes():获取方法形参列表类型;

》method.getExceptionTypes():获取方法抛出异常类型;

  • Constructor

》getDeclaredConstructors():获取运行时类本身所有的构造器;

  • 其他

》获取父类、父类的泛型、接口getInterfaces()、包getPackage()……

  • 调用指定的属性、方法、构造器。

(2)ClassLoader

源程序(.java文件) -> Java编译器 -> 字节码(.class文件) -> 类加载器 -> 字节码校验器 -> 解释器 -> 操作系统平台

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:

  1. 类的加载:将类的class文件读入内存,并为之创建一个java.lang.Class对象,此过程由类加载器完成
  2. 类的连接:将类的二进制数据合并到JRE中
  3. 类的初始化:JVM负责对类进行初始化

类加载器是用来把类(class)装载进内存的。JVM规范定义了两种类型的类加载器:启动类加载器(bootstrap)和用户自定义加载器(user-defined class loader)。JVM在运行时会产生3个类加载器组成的初始化加载器层次结构。

》Bootstrap Classloader

》Extension Classloader

》System Classloader

自底向上检查类是否已加载、自顶向下尝试加载类。

  • 引导类加载器:用c++编写,是JVM自带的类加载器,负责Java平台核心库,用来加载核心类库。该加载类无法直接获取。
  • 扩展类加载器:负责jre/lib/ext目录下的jar包或 -D java.ext.dirs指定目录下的jar包装入工作库
  • 系统类加载器:负责java –classpath或-D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器。
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
class TestClassLoader {
    @Test
    void test() throws Exception {
        // 系统类加载器
        ClassLoader loader1 = ClassLoader.getSystemClassLoader();
        System.out.println(loader1); // sun.misc.Launcher$AppClassLoader@18b4aac2
        // 扩展类加载器
        ClassLoader loader2 = loader1.getParent();
        System.out.println(loader2); // sun.misc.Launcher$ExtClassLoader@2d363fb3
        // 引导类加载器
        ClassLoader loader3 = loader2.getParent();
        System.out.println(loader3); // null

        Class clazz1 = Person.class;
        ClassLoader loader4 = clazz1.getClassLoader();
        System.out.println(loader4); // sun.misc.Launcher$AppClassLoader@18b4aac2

        String className = "java.lang.Object";
        Class clazz2 = Class.forName(className);
        ClassLoader loader5 = clazz2.getClassLoader();
        System.out.println(loader5); // null

        // 获取包里的配置文件
        ClassLoader loader = this.getClass().getClassLoader();
        System.out.println(loader); // sun.misc.Launcher$AppClassLoader@18b4aac2
        InputStream is = loader.getResourceAsStream("jdbc.properties");
        System.out.println(is);
        Properties pros = new Properties();
        pros.load(is);
        String name = pros.getProperty("user");
        System.out.println(name); // root
        String password = pros.getProperty("password");
        System.out.println(password); // 123456
    }
}

(3)动态代理

代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。 按照代理的创建时期,代理类可以分为两种:

  • 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
  • 动态代理:在程序运行时,运用反射机制动态创建而成。

动态代理是指客户通过代理类来调用其它对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象。

代理设计模式的原理:使用一个代理将对象包装起来,然后用代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将该方法调用转到原始对象上。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 定义一个 Subject 类型的接口
interface Subject {
    void hello(String str);

    String bye();
}

// 定义一个类来实现这个接口,这个类就是真实的对象
class RealSubject implements Subject {
    @Override
    public void hello(String str) {
        System.out.println("Hello " + str);
    }

    @Override
    public String bye() {
        System.out.println("Goodbye");
        return "Over";
    }
}

// 动态代理类,必须要实现 InvocationHandler 这个接口
class MyInvocationHandler implements InvocationHandler {
    // 这个就是要代理的真实对象
    Object obj;

    // 构造方法,给要代理的真实对象赋初值,返回一个代理类的对象
    public Object blind(Object obj) {
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object object, Method method, Object[] args)
            throws Throwable {
        System.out.println("Before method"); // AOP增加通用得方法
        // 当代理对象调用真实对象的方法时,
        // 其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
        Object returnVal = method.invoke(obj, args);
        System.out.println("After method"); // AOP增加通用得方法
        System.out.println();
        return returnVal;
    }
}

public class TestDynamicProxy {
    public static void main(String[] args) {
        // 1.要代理的真实对象
        Subject real = new RealSubject();
        // 2.创建一个实现了InvocationHandler接口的类的对象
        MyInvocationHandler handler = new MyInvocationHandler();
        // 3.调用blind()方法,动态的返回一个同样实现了real所在类实现的接口Subject的代理类的对象
        Object obj = handler.blind(real);
        Subject subject = (Subject) obj;
        subject.hello("World");
        subject.bye();
    }
}

// 输出:
// Before method 
// Hello World 
// After method 
// 
// Before method 
// Goodbye 
// After method 

9.网络编程

InetAddress

InetAddress用来代表IP地址,一个InetAdress的对象就代表着一个IP地址。

Socket

  • 利用套接字(Socket)开发网络应用程序成为事实上的标准;
  • 通信的两端都要有Socket,是两台机器间通信的端点;
  • 网络通信其实就是Scoket间的通信;
  • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输;

TCP:

  • Socket和ServerSocket

UDP

  • 类DatagramSocket和DatagramPacket实现了基于UDP协议网络程序;
  • UDP数据报通过数据套接字DatagramSocket发送和接收,系统不保证UDP数据报一定能够安全到达目的地,也不能确定什么时候可以抵达;
  • DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP和端口以及接收端的IP和端口;
  • UDP协议中每个数据报都给出了完整的地址信息,因此无需建立发送方和接收方的连接;

URL

  • URL、URLConnection

References