Java知识大全

待更…

1. 数据类型

boolean flag;
byte a;
short b;
int c;
long d;
float e;
double f;
char g;

2. 输入输出

import java.util.*;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
public class Main{
    public static void main(String args[]){
        Scanner reader = new Scanner(System.in);
        int sum = 0;
        int x;
        int n = reader.nextInt();
        double num = reader.nextDouble();
        for(int i = 1;i <= n; ++ i){
            x = reader.nextInt();
            sum += x;  
        }
        double d = 1.23456789;
        System.out.println("sum=" + sum);
        //printf同C语言%md留足m格%m.nf占m列四舍五入保留n位小数
        System.out.printf("%5d %.3f\n", sum, num);//注意哪怕指定位数也会四舍五入  
        System.out.println(String.format("%.2f", d));//指定位数就不会四舍五入
        System.out.println(String.format("%f", d));//不指定位数就会自动四舍五入
        System.out.println("Hello world");
        student std = new student();
        std.speak("Hello java");
    }
}

class student{
    public void speak(String s){
        System.out.println(s);
    }
}

输入

3 2.345678 1 2 3

输出

sum=6
    6 2.346
1.23
1.234568
Hello world
Hello java

3. 数组

import java.util.*;
import java.math.*;
import java.text.*;

public class Main{
    public static void main(String args[]){
        int a[];
        a = new int[5];
        int b[][];
        b = new int [5007][1007];
        System.out.println(a.length + " " + b.length);
        //int [] a, b[];//表示声明一个一维数组a一个二维数组b
        //b[0] = new int [100];
        //b[1] = new int [25];
        int sum = 0;
        Scanner reader = new Scanner(System.in);
        int n = reader.nextInt();
        for(int i = 1;i <= n; ++ i){
            a[i] = reader.nextInt();  
        }
        for(int i = 1; i <= n; ++ i){
            sum += a[i];
        }
        for(int i : a){//遍历全部
            System.out.println(i);
        }
        //使用println输出char型数组的时候是输出全部元素的值
        char ch[] = {'H', 'e', 'l', 'l', 'o'};
        System.out.println(ch);
        System.out.println("sum = " + sum);
        
    }
}

输入

3 1 2 3

输出

5 5007
0
1
2
3
0
Hello
sum = 6

输入

3 1 2 3

输出

10007 5007
Hello
sum = 6

4. 选择语句

import java.util.*;
import java.math.*;
import java.text.*;

public class Main{
    public static void main(String args[]){
        Scanner read = new Scanner(System.in);
        int n = read.nextInt();
        if(n >= 100){
            System.out.printf("Yes\n");
        }
        else System.out.println("No\n");
        switch (n){
            case 1:
                System.out.println("Yes");
                break;
            case 100:{
                    System.out.println("No");
                    System.out.println("what?");
            }
                break;
            default:
                System.out.println("Hello?");
        }
        
    }
}

输入

100

输出

Yes
No
what?

5. 类

5.1 类

import java.util.*;
import java.math.*;
import java.text.*;

class function{
    int sum = 0;
    static int num;//类变量
    int add(int x, int y){
        return x + y + num;
    }
    int add(int a[]){
        return a[0] + a[1] + num;
    }
    
    int add_all(int a[]){
        int sum = 0;
        for(int tmp : a){
            sum += tmp;
        }
        return sum + num;
    }
    static int max(int a, int b){
        return a > b ? a : b;
    }
}

public class Main{
    public static void main(String args[]){
        function.num = 23330000;//类变量可以直接访问不用new
        Scanner read = new Scanner(System.in);
        int n = read.nextInt();
        int m = read.nextInt();
        Point pos = new Point(1, 2);
        System.out.println(pos.Point());
        Point pos2 = new Point();
        System.out.println(pos2.Point());
        System.out.println(pos2);
        pos2 = pos;
        System.out.println(pos);
        System.out.println(pos2);//原来的pos2已经被垃圾回收系统回收了
        
        function fun = new function();
        
        int res = fun.add(n, m);
        System.out.println(res);
        
        int b[];
        b = new int[3];
        b[0] = 10;
        b[1] = 100;
        b[2] = 1000;
        System.out.println(fun.add(b));//数组也可以直接传过去(java里没有指针)
        System.out.println(fun.add_all(b));
        int ans = fun.max(fun.add(b), fun.add_all(b));
        System.out.println(ans);
    }
}

    class A{
        int sum;
        void f(){
            int sum = 10;
            System.out.println(sum);
        }
    }
    
    class Point {
        int x, y;
        Point (){
            x = 0;
            y = 0;
        }
        Point (int a, int b){
            x = a;
            y = b;
        }
        int Point (){
            return x + y;
        }
    }

import

//package Main;

import java.util.*;
import java.io.*;
import java.lang.*;

public class Main{
    public static void main(String[] args){
        Date date = new Date();
        System.out.println(date.toString());
    }
}

5.2 对象数组

import java.util.*;
import java.io.*;
import java.lang.*;

public class Main{
    public static void main(String[] args){
        Student stu[] = new Student[11];
        for(int i = 1; i  <= 10; ++ i){
            stu[i] = new Student();
            stu[i].number = 100 + i;
        }
        for(int i = 1; i <= 10; ++ i){
            System.out.println(stu[i].number);
        }
    }
}

class Student {
    int number;
}

class tom{
    private double weight;
    protected double age;
    public double hight;
    
}

java支持类里面定义一个类(内部类)

5.3 关键字private,public用处

如果定义为private,那么我们就不能通过new一个类直接调用私密的变量,但是我们可以在类里写一个函数,例如setName,这样我们可以在函数里面防止输入的数据有误,会直接报错。我们也可以调用设置private变量。

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setName("Xiao Ming"); // 设置name
        ming.setAge(12); // 设置age
        System.out.println(ming.getName() + ", " + ming.getAge());
    }
}

class Person {
    private String name;
    private int age;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        if (age < 0 || age > 100) {
            throw new IllegalArgumentException("invalid age value");
        }
        this.age = age;
    }
}

private方法只能由内部方法调用。

5.4 可变参数

可变参数用类型…定义,可变参数相当于数组类型:

class Group {
    private String[] names;

    public void setNames(String... names) {
        this.names = names;
    }
}

完全可以把可变参数改写为String[]类型:

class Group {
    private String[] names;

    public void setNames(String[] names) {
        this.names = names;
    }
}

但是,调用方需要自己先构造String[],比较麻烦。例如:

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 传入1个String[]

另一个问题是,调用方可以传入null:

Group g = new Group();
g.setNames(null);

而可变参数可以保证无法传入null,因为传入0个参数时,接收到的实际值是一个空数组而不是null。

5.5 参数绑定

如果我们传入的是一个数组,那么类似C语言,我们实际上传递的是一个指针(java里没有指针hhh)

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        String[] fullname = new String[] { "Homer", "Simpson" };
        p.setName(fullname); // 传入fullname数组
        System.out.println(p.getName()); // "Homer Simpson"
        fullname[0] = "Bart"; // fullname数组的第一个元素修改为"Bart"
        System.out.println(p.getName()); // "Homer Simpson"还是"Bart Simpson"?
    }
}

class Person {
    private String[] name;

    public String getName() {
        return this.name[0] + " " + this.name[1];
    }

    public void setName(String[] name) {
        this.name = name;
    }
}

输出:

Homer Simpson 
Bart Simpson 

结论:引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。

5.6 构造方法

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

多构造方法
可以定义多个构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this.name = name;
        this.age = 12;
    }

    public Person() {
    }
}

5.7 方法重载

在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。例如,在Hello类中,定义多个hello()方法:

class Hello {
    public void hello() {
        System.out.println("Hello, world!");
    }

    public void hello(String name) {
        System.out.println("Hello, " + name + "!");
    }

    public void hello(String name, int age) {
        if (age < 18) {
            System.out.println("Hi, " + name + "!");
        } else {
            System.out.println("Hello, " + name + "!");
        }
    }
}

这种方法名相同,但各自的参数不同,称为方法重载(Overload)。

6.子类与继承

6.1 继承

Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。

  1. java中只支持单继承,不支持多继承。(其他语言有的支持多继承,例如C++,格式extends 父类1,父类2,…)
  2. 支持多层继承(形成继承体系)。
  3. 子类只能继承父类中的非私有成员,但可以通过父类的成员方法调用父类的私有成员变量。
  4. 子类不能继承父类的构造方法,但可以通过super关键字访问父类的构造方法。

当子类和父类不在同一个包的时候,父类里的private和友好访问权限是不会被继承的。(方法同样也是)

方法重写:参数不变,直接重写函数即可。(会隐藏继承的方法)
参数不同是新写了一个方法不是重写(重载)

注意:子类自动获得了父类的所有字段,严禁定义与父类重名的字段!

//package Main;

import java.util.*;
import java.io.*;
import java.lang.*;

public class Main{
    public static void main(String[] args){
        Student stu1 = new Student();
        stu1.showpeople();
    }
}


class people {
    int age = 10, leg = 2, hand = 2;
    protected void showpeople(){
    System.out.printf("%d岁,%d只脚,%d只手\n", age, leg, hand);
    }
}
class Student extends people {
    int number;
    int add(int x, int y){
        return x + y;
    }
}

6.2 protected关键字

继承有个特点,就是子类无法访问父类的private字段或者private方法。例如,Student类就无法访问Person类的name和age字段:

class Person {
    private String name;
    private int age;
}

class Student extends Person {
    public String hello() {
        return "Hello, " + name; // 编译错误:无法访问name字段
    }
}

这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问

6.3 instanceof运算符

instanceof主要用来判断一个类是否实现了某个接口,或者判断一个实例对象是否属于一个类。

1.判断一个对象是否属于一个类

boolean result = p instanceof Student; 

它的返回值是一个布尔型的。

2.对象类型强制转换前的判断

Person p = new Student();
//判断对象p是否为Student类的实例
if(p instanceof Student)
{
   //向下转型
   Student s = (Student)p;
}

6.4 this 和 super的用法

如果子类声明的成员变量与父类的相同,那么子类继承的父类的成员变量就会被隐藏。
如果需要调用被隐藏的成员变量或者方法可以使用super关键字。

this

this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

this 的用法在 Java 中大体可以分为3种:

  1. 普通的直接引用

这种就不用讲了,this 相当于是指向当前对象本身。

  1. 形参与成员名字重名,用 this 来区分:

实例

class Person {
    private int age = 10;
    public Person(){
    System.out.println("初始化年龄:"+age);
}
    public int GetAge(int age){
        this.age = age;
        return this.age;
    }
}
 
public class test1 {
    public static void main(String[] args) {
        Person Harry = new Person();
        System.out.println("Harry's age is "+Harry.GetAge(12));
    }
}

运行结果:

初始化年龄:10
Harry's age is 12

可以看到,这里 age 是 GetAge 成员方法的形参,this.age 是 Person 类的成员变量。

  1. 引用构造函数

这个和 super 放在一起讲,见下面。

super

super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super 也有三种用法:

  1. 普通的直接引用

与 this 类似,super 相当于是指向当前对象的父类,这样就可以用 super.xxx 来引用父类的成员。

  1. 子类中的成员变量或方法与父类中的成员变量或方法同名

实例

class Country {
    String name;
    void value() {
       name = "China";
    }
}
  
class City extends Country {
    String name;
    void value() {
    name = "Shanghai";
    super.value();      //调用父类的方法
    System.out.println(name);
    System.out.println(super.name);
    }
  
    public static void main(String[] args) {
       City c=new City();
       c.value();
       }
}

运行结果:

Shanghai
China

可以看到,这里既调用了父类的方法,也调用了父类的变量。若不调用父类方法 value(),只调用父类变量 name 的话,则父类 name 值为默认值 null。

  1. 引用构造函数

super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
实例

class Person { 
    public static void prt(String s) { 
       System.out.println(s); 
    } 
   
    Person() { 
       prt("父类·无参数构造方法: "+"A Person."); 
    }//构造方法(1) 
    
    Person(String name) { 
       prt("父类·含一个参数的构造方法: "+"A person's name is " + name); 
    }//构造方法(2) 
} 
    
public class Chinese extends Person { 
    Chinese() { 
       super(); // 调用父类构造方法(1) 
       prt("子类·调用父类"无参数构造方法": "+"A chinese coder."); 
    } 
    
    Chinese(String name) { 
       super(name);// 调用父类具有相同形参的构造方法(2) 
       prt("子类·调用父类"含一个参数的构造方法": "+"his name is " + name); 
    } 
    
    Chinese(String name, int age) { 
       this(name);// 调用具有相同形参的构造方法(3) 
       prt("子类:调用子类具有相同形参的构造方法:his age is " + age); 
    } 
    
    public static void main(String[] args) { 
       Chinese cn = new Chinese(); 
       cn = new Chinese("codersai"); 
       cn = new Chinese("codersai", 18); 
    } 
}

运行结果:

父类·无参数构造方法: A Person.
子类·调用父类”无参数构造方法“: A chinese coder.
父类·含一个参数的构造方法: A person's name is codersai
子类·调用父类”含一个参数的构造方法“: his name is codersai
父类·含一个参数的构造方法: A person's name is codersai
子类·调用父类”含一个参数的构造方法“: his name is codersai
子类:调用子类具有相同形参的构造方法:his age is 18

从本例可以看到,可以用 super 和 this 分别调用父类的构造方法和本类中其他形式的构造方法。

例子中 Chinese 类第三种构造方法调用的是本类中第二种构造方法,而第二种构造方法是调用父类的,因此也要先调用父类的构造方法,再调用本类中第二种,最后是重写第三种构造方法。

6.4.1 super 和 this的异同

super
super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName。例如:

class Student extends Person {
    public String hello() {
        return "Hello, " + super.name;
    }
}
  • super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句)
  • this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)
  • super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) this:它代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名)
  • 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
    super() 和 this() 类似,区别是,super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。
  • super() 和 this() 均需放在构造方法内第一行。
  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • this 和 super 不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。
  • 从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。

这是因为在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(); // 自动调用父类的构造方法
        this.score = score;
    }
}

但是,Person类并没有无参数的构造方法,因此,编译失败。
解决方法是调用Person类存在的某个构造方法。例如:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // 调用父类的构造方法Person(String, int)
        this.score = score;
    }
}

这样就可以正常编译了!
因此我们得出结论:如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。

这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。

  • 继承是面向对象编程的一种强大的代码复用方式;
  • Java只允许单继承,所有类最终的根类是Object;
  • protected允许子类访问父类的字段和方法;
  • 子类的构造方法可以通过super()调用父类的构造方法;
  • 可以安全地向上转型为更抽象的类型;
  • 可以强制向下转型,最好借助instanceof判断;
  • 子类和父类的关系是is,has关系不能用继承。

6.5 final关键字

final类不能被继承,也就是说不能拥有子类
final方法不允许被子类重写。
final +成员变量 = 常量

6.6 继承与多态

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。

Override和Overload不同的是,如果方法签名如果不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。

注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。

加上@Override可以让编译器帮助检查是否进行了正确的覆写。

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态

多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。

多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

package income;

public class come {
    public static void main(String[] args) {
        // 给一个有普通收入,工资收入和享受国务院特殊津贴的小伙伴算税:
        Income[] incomes = new Income[] {
            new Income(3000),
            new Salary(7500),
            new StateCouncilSpecialAllowance(15000)
        };
        System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}

class Income {
    protected double income;

    public Income(double income) {
        this.income = income;
    }

    public double getTax() {
        return income * 0.1; // 税率10%
    }
}

class Salary extends Income {
    public Salary(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        return 0;
    }
}

6.7 覆写Object方法

因为所有的class最终都继承自Object,而Object定义了几个重要的方法:
toString():把instance输出为String;
equals():判断两个instance是否逻辑相等;
hashCode():计算一个instance的哈希值。

调用super
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。

final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承

对于一个类的实例字段(变量),同样可以用final修饰。用final修饰的字段在初始化后不能被修改,可以在构造方法中初始化final字段

class Person {
    public final String name;
    public Person(String name) {
        this.name = name;
    }
}

这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。

6.7 abstract 类和 abstract 方法 (抽象类与抽象方法)

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。
必须把Person类本身也声明为abstract,才能正确编译它:

abstract class Person {
    public abstract void run();
}

抽象类
如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
使用abstract修饰的类就是抽象类。我们无法实例化一个抽象类:

Person p = new Person(); // 编译错误

因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法

尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程

面向抽象编程的本质就是:

  • 上层代码只定义规范(例如:abstract class Person);
  • 不需要子类就可以实现业务逻辑(正常编译);
  • 具体的业务逻辑由不同的子类实现,调用者并不关心。

对于abstract方法只允许声明,不允许实现(因为没有方法体)(毕竟叫抽象,当然不能实实在在的让你实现),并且不允许使用final和abstract同时修饰一个方法或者类,也不允许使用static修饰abstract方法。也就是说,abstract方法只能是实例方法,不能是类方法。

既然abstract类和方法这么特殊,我们就必须对比一下它和普通类与方法之间的区别了:

  1. abstract类中可以有abstract方法

abstract类中可以有abstract方法,也可以有非abstract方法

非abstract类中不可以有abstract方法

  1. abstract类不能使用new运算符创建对象

但是如果一个非抽象类是抽象类的子类,这时候我们想要创建该对象呢,这时候它就必须要重写父类的抽象方法,并且给出方法体,这也就是说明了为什么不允许使用final和abstract同时修饰一个类或者方法的原因。

重点常考!:final和abstract,private和abstract,static和abstract,这些是不能放在一起的修饰符,因为abstract修饰的方法是必须在其子类中实现(覆盖),才能以多态方式调用,以上修饰符在修饰方法时期子类都覆盖不了这个方法,final是不可以覆盖,private是不能够继承到子类,所以也就不能覆盖,static是可以覆盖的,但是在调用时会调用编译时类型的方法,因为调用的是父类的方法,而父类的方法又是抽象的方法,又不能够调用,所以上的修饰符不能放在一起。

  1. abstract类的子类

如果一个非abstract类是abstract类的子类,它必须重写父类的abstract方法,也就是去掉abstract方法的abstract修饰,并给出方法体。

如果一个abstract类是abstract类的子类,它可以重写父类的abstract方法,也可以继承父类的abstract方法。

下面举个例子:

abstract class GirlFriend{                                
//抽象类,封装了两个行为标准
	abstract void speak();
	abstract void cooking();
}

class ChinaGirlFriend extends GirlFriend{
	void speak(){
		System.out.println("你好");	
	}
	void cooking(){
		System.out.println("水煮鱼");
	}	
}

class AmercanGirlFriend extends GirlFriend{
	void speak(){
		System.out.println("hello");
	}
	void cooking(){
		System.out.println("roast beef");
	}
}

class boy{
	GirlFriend friend;
	void setGirlFriend(GirlFriend f){
		friend=f;
	}
	void showGirlFriend(){
		friend.speaking();
		friend.cooking();
	}
}
public class text{
	public static void main(String args[]){
		GirlFriend girl =new ChineseGirlFriend();      //这里girl是上转型对象
		Boy boy=new boy();
		boy.setGirlFriend(girl);
		boy.showGirlFriend();
		girl=new AmericanGirlFriend();             //girl 是上转型对象
		boy.setGirlFriend(girl);
		boy.showGirlFriend();
	}
}

小结

  • 通过abstract定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
  • 定义了抽象方法的class必须被定义为抽象类,从抽象类继承的子类必须实现抽象方法;
  • 如果不实现抽象方法,则该子类仍是一个抽象类;
  • 面向抽象编程使得调用者只关心抽象方法的定义,不关心子类的具体实现。

7. 接口与实现

接口是抽象的抽象(抽象类是具体的抽象)。

例如制作一款运动手表,接口就是产品需要实现的功能。我这款手表要实现与APP的结合,要实现来电的提醒,要实现闹铃的设置,要实现心率的实时监控,要实现步数的记录… 我不会告诉你任何具体的实现方法,我只会给你一个产品功能的框架,而如果你是我团队的一员,要来制作这款运动手表,那么你就一定要把我定义的内容全部实现。

即“如果你是…, ,就必须…”

这就是接口,在程序中,它就相当于是一个类的行为规范。

7.1 接口的作用?

如果一个抽象类没有字段,所有方法全部都是抽象方法:

abstract class Person {
    public abstract void run();
    public abstract String getName();
}

就可以把该抽象类改写为接口:interface。
在Java中,使用interface可以声明一个接口:

interface Person {
    void run();
    String getName();
}

所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。

当一个具体的class去实现一个interface时,需要使用implements关键字。

我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface

class Student implements Person, Hello { // 实现了两个interface
    ...
}

接口继承
一个interface可以继承自另一个interface。interface继承自interface使用extends,它相当于扩展了接口的方法。例如:

interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}

default方法

在接口中,可以定义default方法。例如,把Person接口的run()方法改为default方法:

实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。

接口的作用(引索)

  1. 有利于代码的规范

  2. 有利于代码进行维护

  3. 有利于代码的安全和严密

  4. 丰富了继承的方式

7.3 接口的相关语法

接口声明

​ 关键字:interface public interface 接口名 {}

接口体

  1. 常量(没有变量) ( public static final ) int MAX = 100; 可以省略public static final

  2. 抽象方法 (public abstract) void add(); 可以省略public abstract

​ 常量和抽象方法都只有一种访问修饰符:public

​ 接口默认提供 public,static,final,abstract 关键字

接口的实现

关键字:implements

  1. 类可以实现一个或多个接口 public class Dog implements Eatable,Sleepable

​ Dog 也可以继承一个具体类 public class Dog extends Animal implements Eatable , Sleepable

  1. 类中必须重写接口中的全部方法( 抽象类 可只重写接口中的部分方法)

  2. 类中重写的方法,访问修饰符必须是 public

  3. 接口中定义的常量,在继承了接口的类中可以直接使用。

7.3 接口间的继承

/*
public interface A{}

public interface B extends A{}    // 接口B继承的A
*/
public interface Eatable{     //定义了一个Eatable接口
	void eat();
}
 
public interface Sleepable{    //定义了一个Sleepable接口
	void sleep();
}
 
public class Animal{          // 定义了一个Animal类
	public String name;
	public Animal(String name) {
		this.name = name;
	}
}
 
public Dog extends Animal implements Eatable,Sleepable{    //继承了Animal类,Eatable接口 ,Sleepable方法
	public Dog(String n) {
		this(n);
	}
	public void eat() {         //重写Eatable中的eat方法
		System.out.println(name+"吃骨头");
	}
	public void sleep() {       //重写Sleepable中的sleep方法
		System.out.println(name+"睡得很好");
	}
}

7.4 接口的回调(类似于 对象的上转型对象 )

是什么? 接口名 接口的对象 = 实现了接口的类的对象

​ 该 接口对象 可以调用 被类实现了的 接口方法

public interface Com{}
 
public class Object implements Com{}
 
Com com = new Object();   //接口的回调

7.5 接口与多态

不同的类在实现同一个接口时可能具有不同的实现方式,那么接口变量在回调接口方法时就可能具有多种形态。

7.6 接口参数

将接口的类的实例的引用传递给该接口参数,那么该参数就可以回调类实现的接口方法。

7.8 abstract类和接口类的比较

1.abstract类和接口都可以有abstract方法。
2.接口中只可以有常量,不能有变量;而abstract类中既可以有常量又可以有变量。
3.abstract类中也可以由非abstract方法,接口不可以。

小结

  • Java的接口(interface)定义了纯抽象规范,一个类可以实现多个接口;
  • 接口也是数据类型,适用于向上转型和向下转型;
  • 接口的所有方法都是抽象方法,接口不能定义实例字段;
  • 接口可以定义default方法

8. 异常

Java规定:
必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。

8.1 捕获异常

捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,然后使用catch捕获对应的Exception及其子类:

public class Main {
    public static void main(String[] args) {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) {
        try {
            // 用指定编码转换String为byte[]:
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
            System.out.println(e); // 打印异常信息
            return s.getBytes(); // 尝试使用用默认编码
        }
    }
}

如果我们不捕获UnsupportedEncodingException,会出现编译失败的问题

错误信息类似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且准确地指出需要捕获的语句是return s.getBytes("GBK");。意思是说,像UnsupportedEncodingException这样的Checked Exception,必须被捕获。

toGBK()方法中,因为调用了String.getBytes(String)方法,就必须捕获UnsupportedEncodingException。我们也可以不捕获它,而是在方法定义处用throws表示toGBK()方法可能会抛出UnsupportedEncodingException,就可以让toGBK()方法通过编译器检查,我们在main()方法中捕获异常并处理

public class Main {
    public static void main(String[] args) {
        try {
            byte[] bs = toGBK("中文");
            System.out.println(Arrays.toString(bs));
        } catch (UnsupportedEncodingException e) {
            System.out.println(e);
        }
    }

    static byte[] toGBK(String s) throws UnsupportedEncodingException {
        // 用指定编码转换String为byte[]:
        return s.getBytes("GBK");
    }
}

小结

  • Java使用异常来表示错误,并通过try … catch捕获异常;
  • Java的异常是class,并且从Throwable继承;
  • Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;
  • RuntimeException无需强制捕获,非RuntimeException(Checked - Exception)需强制捕获,或者用throws声明;
  • 不推荐捕获了异常但不进行任何处理。

可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { // 永远捕获不到
        System.out.println("Bad encoding");
    }
}

对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,因为它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行。

因此,正确的写法是把子类放到前面:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (UnsupportedEncodingException e) {
        System.out.println("Bad encoding");
    } catch (IOException e) {
        System.out.println("IO error");
    }
}

Java的try ... catch机制还提供了finally语句,finally语句块保证有无错误都会执行。上述代码可以改写如下:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (UnsupportedEncodingException e) {
        System.out.println("Bad encoding");
    } catch (IOException e) {
        System.out.println("IO error");
    } finally {
        System.out.println("END");
    }
}

注意finally有几个特点:

  1. finally语句不是必须的,可写可不写;
  2. finally总是最后执行。

如果没有发生异常,就正常执行try { ... }语句块,然后执行finally。如果发生了异常,就中断执行try { ... }语句块,然后跳转执行匹配的catch语句块,最后执行finally

可见,finally是用来保证一些代码必须执行的。

捕获多种异常

因为处理IOExceptionNumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
        System.out.println("Bad input");
    } catch (Exception e) {
        System.out.println("Unknown error");
    }
}

小结

使用try ... catch ... finally时:

  • 多个catch语句的匹配顺序非常重要,子类必须放在前面;
  • finally语句保证了有无异常都会执行,它是可选的;
  • 一个catch语句也可以匹配多个非继承关系的异常。

8.2 抛出异常

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try … catch被捕获为止:

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void process1() {
        process2();
    }

    static void process2() {
        Integer.parseInt(null); // 会抛出NumberFormatException
    }
}

通过printStackTrace()可以打印出方法的调用栈,类似:

java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:614)
    at java.base/java.lang.Integer.parseInt(Integer.java:770)
    at Main.process2(Main.java:16)
    at Main.process1(Main.java:12)
    at Main.main(Main.java:5)

printStackTrace()对于调试错误非常有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:

  1. main()调用process1()
  2. process1()调用process2()
  3. process2()调用Integer.parseInt(String)
  4. Integer.parseInt(String)调用Integer.parseInt(String, int)

当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。

如何抛出异常?参考Integer.parseInt()方法,抛出异常分两步:

  1. 创建某个Exception的实例;
  2. throw语句抛出。

下面是一个例子:

void process2(String s) {
    if (s==null) {
        NullPointerException e = new NullPointerException();
        throw e;
    }
}

捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场!

小结

  • 调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;

  • 捕获异常并再次抛出新的异常时,应该持有原始异常信息;

  • 通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception

9. 反射

反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。

反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

9.1 Class类

class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存。

每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。注意:这里的Class类型是一个名叫Classclass。它长这样:

public final class Class {
    private Class() {}
}

String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:

Class cls = new Class(String);

这个Class实例是JVM内部创建的

由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。

这种通过Class实例获取class信息的方法称为反射(Reflection)。

如何获取一个classClass实例?有三个方法:

方法一:直接通过一个class的静态变量class获取:

Class cls = String.class;

方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");

因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例:

Class cls1 = String.class;

String s = "Hello";
Class cls2 = s.getClass();

boolean sameClass = cls1 == cls2; // true

小结

  • JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;

  • 获取一个class对应的Class实例后,就可以获取该class的所有信息;

  • 通过Class实例获取class信息的方法称为反射(Reflection);

  • JVM总是动态加载class,可以在运行期根据条件来控制加载class。

反射

9. 泛型

泛型

10. 集合

集合

11. 多线程

多线程是Java最基本的一种并发模型

即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

多进程模式(每个进程只有一个线程)
多线程模式(一个进程有多个线程)
多进程+多线程模式(复杂度最高)

进程 vs 线程
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
创建进程比创建线程开销大,尤其是在Windows系统上;
进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

Java多线程编程的特点又在于:
多线程模型是Java程序最基本的并发模型;
后续读写网络、数据库、Web开发等都依赖Java多线程模型。

11.1 线程概述

线程共包括以下5种状态。

  1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
  • Thread和Runnable简介

Runnable 是一个接口,该接口中只包含了一个run()方法。它的定义如下:

public interface Runnable {
    public abstract void run();
}

Runnable的作用,实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。

Thread 是一个类。Thread本身就实现了Runnable接口。它的声明如下:

public class Thread implements Runnable {}

Thread的作用,实现多线程。

  • Thread和Runnable的异同点

Thread 和 Runnable 的相同点:都是“多线程的实现方式”。
Thread 和 Runnable 的不同点:
Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。
此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。
通常,建议通过“Runnable”实现多线程!

11.1.1Thread的多线程示例

import java.util.*;

public class Main {
	public static void main(String[] args) {
		ThreadSleep ts1 = new ThreadSleep();
		ThreadSleep ts2 = new ThreadSleep();
		ThreadSleep ts3 = new ThreadSleep();
		
		//ts1.setName("张三");
		//ts2.setName("里斯");
		//ts3.setName("王五");
		
		ts1.start();
		ts2.start();
		ts3.start();
	}
}

class ThreadSleep extends Thread
{
	private int ticket = 10;
	public void run() {
		//System.out.println(getName() + ":" + x + ",日期:" + new Date());
		//睡眠
		//休息1秒钟
		//try {
		//	Tread.sleep(1000);
		///}catch(InterruptedExecption e) {
		//	e.printStackTrace();
		//}
		for(int i = 0; i < 20; ++ i) {
			if(this.ticket > 0) {
				System.out.println(this.getName() + "买票:ticket" + this.ticket  -- );
			}
		}
	}
}

输出:

Thread-0 卖票:ticket10
Thread-0 卖票:ticket9
Thread-0 卖票:ticket8
Thread-0 卖票:ticket7
Thread-0 卖票:ticket6
Thread-0 卖票:ticket5
Thread-0 卖票:ticket4
Thread-0 卖票:ticket3
Thread-0 卖票:ticket2
Thread-0 卖票:ticket1
Thread-1 卖票:ticket10
Thread-1 卖票:ticket9
Thread-1 卖票:ticket8
Thread-1 卖票:ticket7
Thread-1 卖票:ticket6
Thread-1 卖票:ticket5
Thread-1 卖票:ticket4
Thread-1 卖票:ticket3
Thread-1 卖票:ticket2
Thread-1 卖票:ticket1
Thread-2 卖票:ticket10
Thread-2 卖票:ticket9
Thread-2 卖票:ticket8
Thread-2 卖票:ticket7
Thread-2 卖票:ticket6
Thread-2 卖票:ticket5
Thread-2 卖票:ticket4
Thread-2 卖票:ticket3
Thread-2 卖票:ticket2
Thread-2 卖票:ticket1

在AcWing 的在线编译器里每次的输出都不一样…可能是随机的…

结果说明:
(01) ThreadSleep继承于Thread,它是自定义个线程。每个ThreadSleep都会卖出10张票。
(02) 主线程main创建并启动3个ThreadSleep子线程。每个子线程都各自卖出了10张票。

11.1.2 Runnable的多线程示例

通过Runnable实现一个接口,从而实现多线程。

import java.util.*;

public class Main {
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		
		Thread ts1 = new Thread(mt);
		Thread ts2 = new Thread(mt);
		Thread ts3 = new Thread(mt);
		
		ts1.start();
		ts2.start();
		ts3.start();
	}
}

class MyThread implements Runnable
{
	private int ticket = 10;
	public void run() {
		for(int i = 0; i < 20; ++ i) {
			if(this.ticket > 0) {
				System.out.println(Thread.currentThread().getName() + "买票:ticket" + this.ticket  -- );
			}
		}
	}
}

输出:

Thread-0买票:ticket10
Thread-0买票:ticket7
Thread-0买票:ticket6
Thread-2买票:ticket8
Thread-2买票:ticket4
Thread-2买票:ticket3
Thread-2买票:ticket2
Thread-2买票:ticket1
Thread-1买票:ticket9
Thread-0买票:ticket5

11.1.3 start() 和 run()的区别

start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
run() : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!


class MyThread extends Thread{
    public MyThread(String name) {
        super(name);
    }

    public void run(){
        System.out.println(Thread.currentThread().getName()+" is running");
    }
};

public class Demo {
    public static void main(String[] args) {
        Thread mythread=new MyThread("mythread");

        System.out.println(Thread.currentThread().getName()+" call mythread.run()");
        mythread.run();

        System.out.println(Thread.currentThread().getName()+" call mythread.start()");
        mythread.start();
    }
}

运行结果:

main call mythread.run()
main is running
main call mythread.start()
mythread is running

结果说明:
(1) Thread.currentThread().getName()是用于获取“当前线程”的名字。当前线程是指正在cpu中调度执行的线程。
(2) mythread.run()是在“主线程main”中调用的,该run()方法直接运行在“主线程main”上。
(3) mythread.start()会启动“线程mythread”,“线程mythread”启动之后,会调用run()方法;此时的run()方法是运行在“线程mythread”上。

创建线程

Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法

方法一:从Thread派生一个自定义类,然后覆写run()方法

public class Main {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 启动新线程
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

方法二:创建Thread实例时,传入一个Runnable实例:

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 启动新线程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("start new thread!");
    }
}

直接调用Thread实例的run()方法是无效的

线程的优先级

可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

小结

Java用Thread对象表示一个线程,通过调用start()启动一个新线程;

一个线程对象只能调用一次start()方法;

线程的执行代码写在run()方法中;

线程调度由操作系统决定,程序本身无法决定调度顺序;

Thread.sleep()可以把当前线程暂停一段时间。

线程的状态

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

小结

Java线程对象Thread的状态包括:NewRunnableBlockedWaitingTimed WaitingTerminated

通过对另一个线程对象调用join()方法可以等待其执行结束;

可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;

对已经运行结束的线程调用join()方法会立刻返回。

中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        Thread.sleep(1); // 暂停1毫秒
        t.interrupt(); // 中断t线程
        t.join(); // 等待t线程结束
        System.out.println("end");
    }
}

class MyThread extends Thread {
    public void run() {
        int n = 0;
        while (! isInterrupted()) {
            n ++;
            System.out.println(n + " hello!");
        }
    }
}

另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}

注意:共享的变量用关键字volatile声明!

因此,volatile关键字的目的是告诉虚拟机:
每次访问变量时,总是获取主内存的最新值;
每次修改变量后,立刻回写到主内存。
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

小结

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException

目标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程;

通过标志位判断需要正确使用volatile关键字;

volatile关键字解决了共享变量在线程间的可见性问题。

守护线程

如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

小结
守护线程是为其他线程服务的线程;
所有非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有需要关闭的资源(如打开文件等)。

线程同步

Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {
    n = n + 1;
}

synchronized保证了代码块在任意时刻最多只有一个线程能执行。

  • 我们来概括一下如何使用synchronized

    1. 找出修改共享变量的线程代码块;
    2. 选择一个共享实例作为锁;
    3. 使用synchronized(lockObject) { ... }

    在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

    public void add(int m) {
        synchronized (obj) {
            if (m < 0) {
                throw new RuntimeException();
            }
            this.value += m;
        } // 无论有无异常,都会在此释放锁
    }
    

JVM规范定义了几种原子操作:

  • 基本类型(longdouble除外)赋值,例如:int n = m
  • 引用类型赋值,例如:List<String> list = anotherList

小结
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。

同步方法

当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

public void add(int n) {
    synchronized(this) { // 锁住this
        count += n;
    } // 解锁
}
public synchronized void add(int n) { // 锁住this
    count += n;
} // 解锁

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。

小结
用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;
通过合理的设计和数据封装可以让一个类变为“线程安全”;
一个类没有特殊说明,默认不是thread-safe;

12. 考试大纲

在这里插入图片描述

©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页