Java知识大全

it2024-08-16  41

待更…

目录

1. 数据类型2. 输入输出3. 数组4. 选择语句5. 类5.1 类5.2 对象数组5.3 关键字private,public用处5.4 可变参数5.5 参数绑定5.6 构造方法5.7 方法重载 6.子类与继承6.1 继承6.2 protected关键字6.3 instanceof运算符6.4 this 和 super的用法6.4.1 super 和 this的异同 6.5 final关键字6.6 继承与多态6.7 覆写Object方法6.7 abstract 类和 abstract 方法 (抽象类与抽象方法) 7. 接口与实现7.1 接口的作用?7.3 接口的相关语法7.3 接口间的继承7.4 接口的回调(类似于 对象的上转型对象 )7.5 接口与多态7.6 接口参数7.8 abstract类和接口类的比较 8. 异常8.1 捕获异常8.2 抛出异常 9. 反射9.1 Class类 9. 泛型10. 集合11. 多线程11.1 线程概述11.1.1Thread的多线程示例11.1.2 Runnable的多线程示例11.1.3 start() 和 run()的区别 创建线程线程的优先级线程的状态中断线程守护线程线程同步同步方法 12. 考试大纲

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特殊,它没有父类。

java中只支持单继承,不支持多继承。(其他语言有的支持多继承,例如C++,格式extends 父类1,父类2,…)支持多层继承(形成继承体系)。子类只能继承父类中的非私有成员,但可以通过父类的成员方法调用父类的私有成员变量。子类不能继承父类的构造方法,但可以通过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种:

普通的直接引用

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

形参与成员名字重名,用 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 类的成员变量。

引用构造函数

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

super

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

普通的直接引用

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

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

实例

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。

引用构造函数

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类和方法这么特殊,我们就必须对比一下它和普通类与方法之间的区别了:

abstract类中可以有abstract方法

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

非abstract类中不可以有abstract方法

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

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

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

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方法无法访问字段,而抽象类的普通方法可以访问实例字段。

接口的作用(引索)

有利于代码的规范

有利于代码进行维护

有利于代码的安全和严密

丰富了继承的方式

7.3 接口的相关语法

​ 接口声明

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

​ 接口体

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

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

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

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

​ 接口的实现

​ 关键字:implements

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

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

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

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

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

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有几个特点:

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

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

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

捕获多种异常

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

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方法中被抛出的,从下往上看,调用层次依次是:

main()调用process1();process1()调用process2();process2()调用Integer.parseInt(String);Integer.parseInt(String)调用Integer.parseInt(String, int)。

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

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

创建某个Exception的实例;用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类型是一个名叫Class的class。它长这样:

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)。

如何获取一个class的Class实例?有三个方法:

方法一:直接通过一个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为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息;

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

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

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

反射

9. 泛型

泛型

10. 集合

集合

11. 多线程

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

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

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

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

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

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

11.1 线程概述

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

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

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

线程终止的原因有:

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

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

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

小结

Java线程对象Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;

通过对另一个线程对象调用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:

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

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

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

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

基本类型(long和double除外)赋值,例如: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. 考试大纲

最新回复(0)