1 接口和抽象类
2 继承,封装和多态
3 异常处理
4 集合类
5 IO输入流和输出流
6 多线程
参考:《java从入门到项目实践》-李兴华,尚硅谷
一、继承
1.基础
方法重载:
- 方法重载是方法名进行重用的一种技术形式,其最主要的特点为“方法名称相同,参数的类型或个数不同”,在调用时会根据传递的参数类型和个数不同执行不同的方法体。
构造方法:
- 定义:
(1).构造方法的名称和类名称保持一致。
(2).构造方法不允许有返回值类型声明。 - 其主要功能是完成对象属性的初始化操作。
- 构造方法是在实例化对象的时候使用,而普通方法是在实例化对象产生之后使用的。
- 由于对象实例化操作一定需要构造方法的存在,所以如果在类之中没有明确定义构造方法的话,则会自动生成一个无参数并且无返回值的构造方法,供用户使用;如果一个类中已经明确定义了一个构造方法则不会自动生成无参且无返回值的构造方法,也就是说,一个类中至少存在一个构造方法。
构造方法重载
- code
package com.yao.practice;
public class JavaDemo {
public static void main(String[] args) {
Person per = new Person("哈");
per.tell();
}
}
class Person { //定义一个类
private String name;
private int age;
public Person() { //【构造方法重载】定义无参构造方法
name = "嘿";
age = 10;
}
public Person(String name) { //【构造方法重载】定义单参构造方法
this.name = name;
}
public Person(String name, int age) { //【构造方法重载】定义双参构造方法
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void tell() {
System.out.println("姓名:" + name + "、年龄:" + age);
}
}
程序执行结果:
姓名:哈、年龄:0
- 本程序类中针对Person类的构造方法进行了重载,分别定义了无参数构造、单参构造、双参构造,这样在进行对象实例化的时候就可以通过不同的构造方法来进行属性初始化内容的设置。
- 在本程序中通过Person类的有参构造方法在类对象实例化的时候,就可以实现name与age属性内容的初始化,这样可以减少setter()方法的调用,以实现简化代码的目的。然而setter()方法除了拥有初始化属性内容的功能之外,也可以实现修改内容的功能,所以在类定义中是必不可少的。
2.面向对象继承性
继承
- 语法:
class 子类 extends 父类 {}
- 继承实现的主要目的是子类可以重复使用父类中的结构,同时可以根据子类功能的需要进行结构扩充,所以子类往往要比父类描述的范围更小。
- code1:继承基本实现
public class JavaInheritance {
public static void main(String[] args) {
Student stu = new Student();
stu.setName("嘿嘿");
stu.setAge(18);
System.out.println("姓名:" + stu.getName() + "、年龄:" + stu.getAge());
}
}
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student extends Person {
// 在子类中不定义任何的功能
}
程序执行结果:
姓名:嘿嘿、年龄:18
- code2:在子类中扩充父类的功能
package com.yao.practice;
public class JavaInheritance2 {
public static void main(String[] args) {
Student2 stu = new Student2();
stu.setName("嘿嘿"); //父类定义
stu.setAge(18); //父类定义
stu.setSchool("NEU"); //子类扩充方法
System.out.println("姓名:" + stu.getName() + "、年龄:" + stu.getAge() + "、学校:" + stu.getSchool());
}
}
class Person3 {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student2 extends Person3 { //Student是子类
private String school; //子类扩充的属性
public String getSchool() { //扩充的方法
return school;
}
public void setSchool(String school) { //扩充的方法
this.school = school;
}
}
程序执行结果:
姓名:嘿嘿、年龄:18、学校:NEU
子类对象实例化流程
- code1:子类对象实例化,观察无参构造调用
package com.yao.practice;
public class JavaInheritance3 {
public static void main(String[] args) {
Student3 stu = new Student3(); //实例化子类对象
}
}
class Person4 {
public Person4() { // 父类无参构造
System.out.println("【Person父类】调用Person4父类构造实例化对象(public Person4())");
}
}
class Student3 extends Person4 { //Student3继承父类
public Student3() { //子类无参构造
System.out.println("【Student3子类】调用Student3子类构造实例化对象(public Student3())");
}
}
程序执行结果:
【Person4父类】调用Person4父类构造实例化对象(public Person4())
【Student3子类】调用Student3子类构造实例化对象(public Student3())
@ 在继承结构中,子类需要重用父类中的结构,所以在进行子类对象实例化之前往往都会默认调用父类中的无参构造方法,为父类对象实例化(属性初始化),而后再进行子类构造调用,为子类对象实例化(属性初始化)。
@ 本程序在实例化Student3子类对象时只调用了子类构造,而通过执行结果可以发现,父类构造会被默认调用,执行完毕后才调用了子类构造,所以可以得出结论:子类对象实例化前一定会实例化父类对象,实际上这个时候就相当于子类的构造方法里面隐含了一个super()的形式。
- code2:观察子类构造:
class Student3 extends Person4 { //Student继承父类
public Student3() { //子类无参构造
super(); //明确调用父类构造,不编写时会默认找到父类无参构造
System.out.println("【Student子类】调用Student子类构造实例化对象(public Student())");
}
}
@ 子类中的super()的作用表示在子类中明确调用父类的无参构造,如果不写也默认会调用父类构造,对于super()构造调用的语句只能够在子类的构造方法中定义,并且必须放在子类构造方法的首行。如果父类没有提供无参构造方法,就可以通过“super(参数,...)”的形式调用指定的构造方法。
- code3:明确调用父类指定的构造方法:
package com.yao.practice;
public class JavaInheritance4 {
public static void main(String[] args) {
Student4 stu = new Student4("嘿", 18, "NEU");
}
}
class Person5 {
private String name;
private int age;
public Person5(String name, int age) { //父类不再提供无参构造器
this.name = name;
this.age = age;
}
}
class Student4 extends Person5 {
private String school;
public Student4(String name, int age, String school) { //子类构造
super(name, age); //必须明确用其父类有参构造
this.school = school;
}
}
@ 本程序Person5父类不再明确提供无参构造方法,这样在子类构造方法中就必须通过super()明确指明要调用的父类构造,并且该语句必须放在子类构造方法的首行。
在程序中,实例化就表示对象的出生,所以子类未出生之前(实例化之前),父类对象一定要先出生(默认调用父类构造,实例化父类对象)
对比以下两段程序与上述程序code3:
package com.yao.practice;
public class JavaInheritance4 {
public static void main(String[] args) {
Student4 stu = new Student4("嘿", 18);
System.out.println("姓名:" + stu.getName() + "、年龄:" + stu.getAge());
}
}
class Person5 {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student4 extends Person5 {
public Student4(String name, int age) { //子类构造
}
}
程序执行结果:
姓名:null、年龄:0
package com.yao.practice;
public class JavaInheritance4 {
public static void main(String[] args) {
Student4 stu = new Student4("嘿", 18);
System.out.println("姓名:" + stu.getName() + "、年龄:" + stu.getAge());
}
}
class Person5 { //父类构造,提供了一个无参构造方法
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Student4 extends Person5 {
public Student4(String name, int age) { //子类构造
setName(name); //调用父类构造,设置name属性内容
setAge(age); //调用父类构造,设置age属性内容
}
}
程序执行结果:
姓名:嘿、年龄:18
继承限制
(1).一个子类只能继承一个父类,存在单继承局限。java中允许多层继承,不允许多重继承。
(2).在一个子类继承的时候,实际上会继承父类的所有操作(属性、方法),但是需要注意的是,对于所有的非私有(no private)操作属于显示继承(可以直接利用对象操作),而所有的私有(private)操作属于隐式继承(间接完成)。
3.覆写
方法覆写
- 在类继承结构中,子类可以继承父类中的全部方法,当父类某些方法无法满足子类设计需求时,就可以针对已有的方法进行扩充,那么此时在子类中定义与父类中方法名称、返回值类型、参数类型及个数完全相同的方法的时候,称为方法覆写。
方法覆写限制
- 被子类所覆写的方法不能拥有比父类更严格的访问控制权限:private<default(默认)<public。
- 父类方法定义private时,子类方法无法覆写此方法。
属性覆盖
- 子类除了可以对父类中的方法进行覆写外,也可以对非private定义的父类属性进行覆盖,此时只需定义与父类中成员一致的名称即可。
- code:属性覆盖
package com.yao.practice;
public class Overriding {
public static void main(String[] args) {
DatabaseChannel channel = new DatabaseChannel(); // 实例化子类方法
channel.fun(); // 子类扩充方法
}
}
class Channel {
String info = "ha"; // 非私有属性
}
class DatabaseChannel extends Channel {
int info = 18; // 名称相同,类型不同
public void fun() {
System.out.println("【父类info成员属性】" + super.info);
System.out.println("【父类info成员属性】" + this.info);
}
}
程序执行结果:
【父类info成员属性】ha
【父类info成员属性】18
|
this与supper的区别
this与super调用构造方法时必须都放在构造方法的首行,但是不管如何调用子类一定会有一个构造方法调用父类构造。 |
4.final关键字
- 功能:定义不能够被继承的类,定义不能够被覆写的方法,定义常量(全局常量)。
- 在final关键字里有一个重要的应用技术:可以利用其定义常量,常量的内容一旦定义则不可修改。在java程序中为了将常量与普通成员属性进行区分,所以要求常量名称字母全部大写。大部分的系统设计中,常量往往都会作为一些全局的标记使用,所以在进行常量定义时,往往都会利用public static final的组合来定义全局常量。
5.this关键字
三种调用
|
1.当前类中的属性:this.属性。【严格意义来讲,指的就是调用当前对象中的属性】
2.当前类中的方法(普通方法、构造方法):this()、this.方法名称()。 3.描述当前对象。 |
this调用本类属性
当通过setter()或者是构造方法为类中的成员设置内容时,为了可以清楚地描述出具体参数与成员属性的关系,往往会使用同样的名称,那么此时就需要通过this来描述类的成员属性。
this调用本类方法
- 调用本类普通方法:可以使用“this.方法()”调用,并且可以在构造方法与普通方法中使用。
- 调用本类构造方法:调用本类其他构造方法使用this()形式,此语句只允许放在构造方法首行使用。
class Person{
private String name;
private int age;
public Person(String name,int age){
this.setName(name); //调用本类setName()方法
setAge(age); //不使用“this.方法()”也表示调用本类方法
}
public void tell(){
System.out.println("姓名:"+this.name+"、年龄"+this.age);
}
public String getName() {
return Name;
}
public void setName(String Name) {
this.Name = Name;
}
public String getAge() {
return Age;
}
public void setType(int Age) {
this.Age = Age;
}
}
public class JavaDemo{
public static void main(){
Person per=new Person("张三,12");
per.tell();
}
}
程序执行结果:
姓名:张三、年龄12
@ 本程序在构造方法中调用了本类中的普通方法,由于是在本类,所以是否使用this没有明确的要求,但是从标准性的角度来讲还是采用“this.方法()”的形式更加合理。
@ 当一个类中存在有若干个构造方法时也可以利用this()的形式实现构造方法之间的调用,但是需要记住的是,该语句只能放在构造方法的首行。
@ 如果一个类中存在了多个构造方法的话,并且这些构造方法都使用了this()相互调用,那么至少要保留一个构造方法没有调用其他构造,以作为程序的出口。
- 错误的构造方法调用:出现了构造方法的递归调用
//错误的构造方法调用:出现了构造方法的递归调用
package com.yao.practice;
class Person6 {
private String name;
private int age;
public Person6() {// 无参构造
this("ha", 18);// 错误提示:Recursive constructor invocation Person6(String, int)
System.out.println("实例化Person6");
}
public Person6(String name) {// 单参构造
this();// 错误提示:Recursive constructor invocation Person6()
this.name = name;
}
public Person6(String name, int age) {// 两参构造
this(name);// 错误提示:Recursive constructor invocation Person6(String)
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getInfo() {
return "姓名:" + this.name + ",年龄:" + this.age;
}
}
this表示当前对象
- code 实现消息发送逻辑
package com.yao.practice;
class Message {
private Channel2 channel;// 保存消息发送通道
private String title;// 消息标题
private String content;// 消息内容
// 【4】调用此构造实例化,此时的channel=主类ch
public Message(Channel2 channel, String title, String content) {
this.channel = channel;// 保存消息熔断
this.title = title;// 设置title属性
this.content = content;// 设置content属性
}
public void send() {
// 【6】判断当前通道是否可用,那么此时的this.channel就是主类中的ch
if (this.channel.isConnect()) {// 连接成功
System.out.println("【消息发送】title=" + this.title + "、content=" + this.content);
} else {// 没有连接
System.out.println("【ERROR】没有可用的连接通道,无法进行消息发送");
}
}
}
class Channel2 {
private Message message;// 消息发送由Message负责
// 【2】实例化Channel对象,调用构造方法,接收要发送的消息标题与消息内容
public Channel2(String title, String content) {
// 【3】实例化Message,但是需要将主类中的ch传递到Message中、this=ch
this.message = new Message(this, title, content);
// 【5】消息发送
this.message.send();
}
public boolean isConnect() {// 判断连接是否创建
return true;// 默认返回true
}
}
public class Test {
public static void main(String[] args) {
// 【1】实例化一个Channel对象,并且传入要发送的消息标题与消息内容
Channel2 ch = new Channel2("hei", "lalalala");// 实例化Channel类对象并发送消息
}
}
二、多态
1.简介
- 展现形式1:方法的多态性
方法的重载
方法的覆写(重写) - 展现形式2:对象的多态性(父类与子类之间的转换处理)。
对象向上转型:父类 父类实例 = 子类实例,自动完成转换。
对象向下转型:子类 子类实例 = (子类) 父类实例,强制完成转换。
对象的多态性和方法重写是紧密联系在一起的。
2.对象向上转型
- 在子类对象实例化之前一定会自动实例化父类对象,所以此时子类对象的实例化通过父类进行接收即可实现对象的自动向上转型。而此时的本质还是子类实例,一旦子类重写了父类方法,并且调用该方法时,所调用的一定是被子类重写过的方法。向上转型时,子类单独定义的方法会丢失。
- code:对象向上转型(无需关心对象的声明类型,关键在于实例化新对象时所调用的那个子类的构造)
package com.yao.practice2;
public class Demo {
public static void main(String[] args) {
Person p = new SuperMan(); //向上转型
p.run(); //调用被重写的方法
}
}
class Person {
public void run() { //父类定义run()方法
System.out.println("跑");
}
}
class SuperMan extends Person { //【方法重写】子类有不同的方法体
public void run() {
System.out.println("闪现");
}
}
程序执行结果:
闪现
@ 对象向上转型的最大特点在于其可以通过父类对象的自动接收子类实例,而在实际的项目开发中,就可以利用这一原则实现方法接收或返回参数类型的统一。另外,一旦发生了对象的向上转型,那么父类对象可以使用的方法只能是本类或其父类定义的方法,是无法直接调用子类扩充方法的。
3.对象向下转型
- 子类继承父类后可以对已有的父类功能进行扩充,除了采用方法重写外,子类也可以定义属于自己的新方法。而对于子类扩充的方法只有具体的子类实例才可以调用。在这样的的情况下,如果子类已经发生了向上转型后就需要通过强制向下转型来实现子类扩充方法的调用。
- code:
package com.yao.practice2;
public class Demo {
public static void main(String[] args) {
Person p = new SuperMan(); // 向上转型
p.run(); // 调用被重写的方法
SuperMan spm = (SuperMan) p;//强制转为子类实例
spm.fire(); //子类扩充方法
}
}
class Person {
public void run() { // 父类定义run()方法
System.out.println("跑");
}
}
class SuperMan extends Person { // 子类扩充方法
public void fire() {
System.out.println("喷火");
}
}
程序执行结果:
跑
喷火
| 必须先发生向上转型,之后才可以进行向下转型:在对象向下转型中,父类实例是不可能强制转换为任意子类实例,必须先通过子类实例化,利用向上转型让父类对象与具体子类实例之间发生联系之后才可以向下转型,否则将出现ClassCastException异常。 |
4.instanceof关键字:实例类型判断
语法
| 对象 instanceof 类 |
该判断返回一个boolean类型数据,如果是true表示实例是指定类对象。
5.Object类
- Object类是唯一一个没有父类的类,是所有类的父类。所有的引用数据类型都可以向Object类进行向上转型,利用Object类可以实现方法接收参数或返回数据类型的统一。
获取对象信息
在Object类中提供有一个同toString()方法,利用此方法可以实现对象的信息获取,而该方法在直接进行对象输出时默认被调用的。
Object.toString()方法的默认实现:
//当一个对象被直接输出时,默认输出的是对象编码(或者理解为内存地址数值),
//这是因为Object类是所有类的父类,但是不同的类可能有不同样式的对象信息获取,
//为此Object类考虑到公共设计就使用一个对象编码的形式展示,可以观察toString()的源码:
public String toString(){
return getClass().getName()+"@"+Integer.toHexString((hashCode()));
}
//此时toString()方法利用相应的反射机制和对象编码获取了一个对象信息,
//所以当子类不重写toString()方法是toString()方法会返回“类名称@7b1d7fff”类似的信息。
对象比较
- 所谓对象比较的主要功能是比较两个对象内容是否完全相同。
- 对象比较标准方法
对象比较标准方法: public boolean equals(Object obj)
两种比较方法“==”与“equals”
1.==操作符
|
(1).基本类型比较值:只要两个变量的值相等,记为true。
(2).引用类型比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。 (3).用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错 |
2.equals()方法
(1).equals():所有类都继承了Object,也就获得了equals()方法,还可以重写。
(2). 特例:当用equals()方法进行比较时,对类File、String、Date及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象;
(3).当自定义使用equals()时, 可以重写。 用于比较两个对象的“内容” 是否都相等。 |
3.重写equals()方法的原则
|
(1).对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
(2).自反性:x.equals(x)必须返回是“true”。 (3).传递性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。 (4).一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。 (5).任何情况下,x.equals(null),永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。 |
4.==和equals的区别
|
(1). == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址。
(2). equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。 (3). 具体要看自定义类里有没有重写Object的equals方法来判断。 (4). 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。 |
5.==和equals练习题
int it = 65;
float fl = 65.0f;
System.out.println(“65和65.0f是否相等? ” + (it == fl)); //true
char ch1 = 'A'; char ch2 = 12;
System.out.println("65和'A'是否相等? " + (it == ch1));//true
System.out.println(“12和ch2是否相等? " + (12 == ch2));//true
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("str1和str2是否相等? "+ (str1 == str2));//false
System.out.println("str1是否equals str2? "+(str1.equals(str2)));//true
System.out.println(“hello” == new java.util.Date()); //编译不通过
三、抽象类
1.定义
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。抽象类需要使用abstract进行定义,并且在一个抽象类中可以利用abstract关键字定义若干个抽象方法。
2.abstract关键词
abstract修饰类:抽象类
| 1.此类不能实例化
2.抽象类中一定有构造器,便于子类实例化时调用 3.开发中,都会提供抽象的子类,让子类对象实例化,完成相关的操作。 |
abstract修饰方法:抽象方法
|
1.抽象方法只有方法的声明,没有方法体(“{内的代码为方法体}”)
2.包含抽象方法的类一定是一个抽象类(接口???),反之,抽象类中可以没有抽象方法。 3.若子类重写了父类中的所有抽象方法后,此子类方法可实例化;若子类没有重写父类中的所有的抽象方法,则此类也是个抽象类,需要abstract修饰。 |
抽象类相关说明:
|
1.抽象类必须由子类继承,所以在定义时不允许使用final关键字定义抽象类或抽象方法。
2.抽象类中可以定义成员属性与普通方法,为了可以为抽象类中的成员属性初始化,可以在抽象类中提供构造方法。子类在继承抽象类时会默认调用父类的无参构造,如果抽象类没有提供无参构造方法,则子类必须通过super()的形式调用指定参数的构造方法。 @ 本程序在Message抽象类中定义了一个单参构造方法,由于父类没有明确提供无参构造,所以DatabaseMessage子类的构造方法中就必须通过super(type)语句形式明确调用父类构造。 3.抽象类中允许没有抽象方法,即便没有抽象方法,也无法直接使用关键字new直接实例化抽象类对象。 4.抽象类中可以提供static方法,并且该类方法不受到抽象实例化对象的限制。 |
四.包装类
1.引出
java是一门面向对象的语言,所有的设计都是围绕着对象这一核心概念展开的,但与这一设计有所违背的就是基本数据类型(byte、short、int、long、float、double、char、boolean),所以为了符合这一特点可以利用类的结构对基本数据类型进行包装。java设计了8个包装类。

2.装箱与拆箱
数据装箱
将基本数据类型保存到包装类中,一般可以利用包装类的构造方法完成。
数据拆箱
调用包装类的.xxxValue()方法,从包装类中获取基本数据类型。
public class JavaDemo{
public static void main(String args[]){
Integer obj = new Integer(10); //装箱
int num = obj.intValue(); //拆箱
System.out.println(num*num); //数值计算
}
}
以上操作是在JDK1.5之前所进行的必须的操作,但是在JDK1.5之后,java提供了自动装箱和拆箱机制,并且包装类的对象可以自动进行数学计算了。
public class JavaDemo{
public static void main(String args[]){
Integer obj = 10; //自动装箱
int num = obj; //自动拆箱
obj++; //包装类对象可以直接参与数学运算
System.out.println(num*obj); //直接参与数值计算
}
}
3.数据类型转换
字符串转换成基本数据类型
|
(1).通过包装类的构造器实现:int i = new Integer(“12”);
(2).通过包装类的parseXxx(String s)静态方法:Float f = Float.parseFloat(“12.1”); |
基本数据类型转换成字符串
|
(1).调用字符串重载的valueOf()方法:String fstr = String.valueOf(2.34f);
(2).更直接的方式:String intStr = 5 + “” |
基本类型、包装类与String类间的转换

五.接口
1.概述
- 一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。
- 另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a(继承)的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等都支持USB连接。
- 接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要...则必须能...”的思想。继承是一个"是不是"的关系,而接口实现则是 "能不能"的关系。
- 接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守。
2.接口
- 在接口中可以定义:
JDK8以前:全局常量、抽象方法。
JDK8及之后:全局常量、抽象方法、default方法以及static方法。 - 特点:
|
- extends、implements关键字的顺序
class 子类 extends 父类 implements 接口1,接口2,...{}
- 接口使用:
|
- 利用接口定义标准
public class InterfaceTest {
public static void main(String[] args) {
Computer com = new Computer();// 实例化计算机类对象
com.plugin(new Keyboard());// IUSB key=new Keyboard();com.plugin(key);多态性
com.plugin(new Print());// IUSB printer=new Print();com.plugin(printer);多态性
}
}
interface IUSB { // 定义USB标准
public boolean check();// 【抽象方法】检查通过可以工作
public void work();// 【抽象反复】设备工作
}
class Computer {// 定义计算机类
public void plugin(IUSB usb) {// 计算机上使用USB标准设备
if (usb.check()) {// 检查设备
usb.work();// 开始工作
} else {// 检查失败
System.out.println("硬件设备安装出了问题,无法使用!");
}
}
}
class Keyboard implements IUSB {// USB子类
public boolean check() {// 覆写抽象方法
return true;
}
public void work() {// 覆写抽象方法
System.out.println("打开计算机在线学习,输入:haha");
}
}
class Print implements IUSB { // USB子类
public boolean check() { // 覆写抽象方法
return false;
}
public void work() {// 覆写抽象方法
System.out.println("打印haha");
}
}
六、异常处理
1.基本概念
-
在程序开发中,程序的编译与运行是两个不同的阶段,编译主要针对的是语法检测,而在程序运行时却有可能出现各种各样的错误导致程序中断执行,那么这些错误在java中统一称为异常。
-
异常:在Java语言中,将程序执行中发生的不正常情况称为“异常” 。(开发过程中的语法错误和逻辑错误不是异常)
-
Java程序在执行过程中所发生的异常事件可分为两类:
- Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError和OOM。一般不编写针对性的代码进行处理。
- Exception:其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:
1). 空指针访问
2). 试图读取不存在的文件
3). 网络连接中断
4). 数组角标越
-
对于这些错误,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。
-
捕获错误最理想的是在编译期间,但有的错误只有在运行时才会发生。
1. 运行时异常:- 是指编译器不要求强制处置的异常。一般是指编程时的逻辑错误,是程序员应该积极避免其出现的异常。java.lang.RuntimeException类及它的子类都是运行时异常。
- 对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
2. 编译时异常:
- 是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。编译器要求Java程序必须捕获或声明所有编译时异常。
- 对于这类异常,如果程序不处理,可能会带来意想不到的结果。
2.常见异常
- java.lang.RuntimeException
- ClassCastException
- ArrayIndexOutOfBoundsException
- NullPointerException
- ArithmeticException
- NumberFormatException
- InputMismatchException
- java.io.IOExeption
- FileNotFoundException
- EOFException
- java.lang.ClassNotFoundException
- java.lang.InterruptedException
- java.io.FileNotFoundException
- java.sql.SQLException
ArrayIndexOutOfBoundsException
package com.yao.practice;
public class IndexOutExp {
public static void main(String[] args) {
String friends[] = { "lisa", "bily", "kessy" };
for (int i = 0; i < 5; i++) {
System.out.println(friends[i]); // friends[4]?
}
System.out.println("\nthis is the end");
}
}
--------------------------------------------------------------------------------------------------------------------
运行结果:
Exception in thread "main" lisa
bily
kessy
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at com.yao.practice.IndexOutExp.main(IndexOutExp.java:7)
NullPointerException
public class NullRef {
int i = 1;
public static void main(String[] args) {
NullRef t = new NullRef();
t = null;
System.out.println(t.i);
}
}
--------------------------------------------------------------------------------------------------------------------
运行结果:
Exception in thread "main" java.lang.NullPointerException
at com.yao.practice.NullRef.main(NullRef.java:8)
ArithmeticException
package com.yao.practice;
public class DivideZero {
int x;
public static void main(String[] args) {
int y;
DivideZero c = new DivideZero();
y = 3 / c.x;
System.out.println("program ends ok!");
}
}
--------------------------------------------------------------------------------------------------------------------
运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.yao.practice.DivideZero.main(DivideZero.java:9)
ClassCastException
package com.yao.practice;
import java.util.Date;
public class Order {
public static void main(String[] args) {
Object obj = new Date();
Order order;
order = (Order) obj;
System.out.println(order);
}
}
--------------------------------------------------------------------------------------------------------------------
运行结果:
Exception in thread "main" java.lang.ClassCastException: class java.util.Date cannot be cast to class com.yao.practice.Order (java.util.Date is in module java.base of loader 'bootstrap'; com.yao.practice.Order is in unnamed module of loader 'app')
at com.yao.practice.Order.main(Order.java:9)
3.异常处理机制一:try-catch-finally
try、catch、finally异常处理格式
- try 捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。
- catch(ExceptionName1 e) 在catch语句块中是对异常对象进行处理的代码。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。
- finally
- 捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。
- 不论在try代码块中是否发生了异常事件, catch语句是否执行, catch语句是否有异常, catch语句中是否有return,finally块中的语句都会被执行。
- finally语句和catch语句是任选的,finally的作用往往是在开发中进行一些资源释放操作。
try{
......//可能产生异常的代码
}
catch(ExceptionName1 e){
.....//当产生ExceptionName1型异常时的处置措施
}
catch(ExceptionName2 e){
......//当产生ExceptionName2型异常时的处置措施
}finally{
......//无论是否发生异常, 都无条件执行的语句
}
@ 在以上格式中catch与finally都是可选的,但是这两个语句不可以同时消失,组合形式有:try...catch...finally、try...catch、try...finally。
- 捕获异常的有关信息
- getMessage() 获取异常信息,返回字符串
- printStackTrace() 获取异常类名和异常信息,以及异常出现在程序中的位置。返回值void。
package com.yao.practice;
public class Test2 {
public static void main(String[] args) {
System.out.println("【1】******程序开始执行******");
try {
System.out.println("【2】******数学计算:" + (10 / 0));
} catch (ArithmeticException e) {
// System.out.println("【c】处理异常:"+e);
e.printStackTrace();
}
System.out.println("【3】******程序执行完毕******");
}
}
运行结果:
【1】******程序开始执行******
java.lang.ArithmeticException: / by zero
at com.yao.practice.Test2.main(Test2.java:8)
【3】******程序执行完毕******
4.异常处理机制二: 声明抛出异常
- 声明抛出异常是Java中处理异常的第二种方式
- 如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常, 则此方法应显示地声明抛出异常, 表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。
- 在方法声明中用throws语句可以声明抛出异常的列表, throws后面的异常类型可以是方法中产生的异常类型, 也可以是它的父类。
5.手动抛出异常
- Java异常类对象除在程序执行过程中出现异常时由系统自动生成并
抛出, 也可根据需要使用人工创建并抛出。
- 首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
IOException e = new IOException();
throw e; - 可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将会产生语法错误:
throw new String("want to throw");
- 首先要生成异常类对象,然后通过throw语句实现抛出操作(提交给Java运行环境)。
- throw与throws的区别
- throw:是在代码块中使用的,主要是手动进行异常对象的抛出。
- throws:是在方法中定义使用的,表示将此方法中可能产生的异常明确告诉给调用出,有调用处进行处理。
6.用户自定义异常类
七、多线程
1.基本概念
- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期。
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域 - 进程可进一步细化为线程,是一个程序内部的一条执行路径。(java中main方法对应一个线程)
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间—>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使 得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

- 单核CPU和多核CPU的理解
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。(一个厨师做菜)
如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。 - 并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时(看似同时,CPU快速的切换)执行多个任务。比如:秒杀、多个人做同一件事。
2.多线程的优点
背景: 以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短(因为CPU在不同线程之间切换会消耗时间),为何仍需多线程呢?- 多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
- 何时需要多线程:
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
3.线程的创建和使用
JDK1.5之前创建新执行线程有两种方法:
继承Thread类的方式
实现Runnable接口的方式
方式一: 继承Thread类
- 方式一: 继承Thread类
1). 定义子类继承Thread类。
2). 子类中重写Thread类中的run方法。
3). 创建Thread子类对象,即创建了线程对象。
4). 调用线程对象start方法:启动线程,调用run方法。 - 注意点:
1).如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
2).run()方法由JVM调用,什么时候调用,执行的过程控制都由操作系统的CPU调度决定。
3).想要启动多线程,必须调用start方法。
4).一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“ IllegalThreadStateException”。
package com.yao.practice2;
/**
* 多线程的创建,方式一:继承于Thread类 1.创建一个继承于Thread的子类
* 2.重写Thread类的run()-->将此线程执行的操作声明在run()方法中 3.创建Thread类的子类对象 4.通过此对象调用start()
*
*
* 例子:遍历100以内所有的偶数
*
* @author YAO
*
*/
//1.创建一个继承于Thread的子类
class MyThread extends Thread {
// 2.重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread t1 = new MyThread();
// 4.通过此对象调用start():①启动当前线程②调用当前线程的run()
t1.start();
// 问题一:不能通过直接调用run()的方式启动线程。
// t1.run();
// 问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行,会报IllegalThreadStateException
// t1.start();
// 我们需要重新创建一个线程的对象
MyThread t2 = new MyThread();
t2.start();
// 如下操作仍然是在main线程中执行的。
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
- 创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
package com.yao.practice2;
/**
* 练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
*
* @author YAO
*/
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
// 方法一:创建对象
// MyThread1 m1 = new MyThread1();
// MyThread2 m2 = new MyThread2();
//
//
// m1.start();
// m2.start();
// 方法二:创建Thread类的匿名子类的方式
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
- 例子:创建三个窗口卖票,总票数为100张
package com.yao.practice2;
/**
* 例子:创建三个窗口卖票,总票数为100张,继承Thread类
* 存在线程安全问题,待解决
* @author YAO
*
*/
class Window extends Thread {
private static int ticket = 100; // 关键字 static的作用
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1"); // 为各个线程创建名字
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
以上程序存在线程安全问题:

方式二: 实现Runnable接口
- 方式二: 实现Runnable接口
1). 定义子类,实现Runnable接口。
2). 子类中重写Runnable接口中的run方法。
3). 通过Thread类含参构造器创建线程对象。
4). 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
5). 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
package com.yao.practice2;
/**
* 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
* 存在线程安全问题,待解决
*
* @author YAO
*
*/
class Window2 implements Runnable {
private int ticket = 100; //
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1"); // 为各个线程创建名字
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
继承方式和实现方式的联系与区别
- 实现方式的好处(开发中优先选择:实现Runnable接口的方式 )
实现的方式没有类的单继承性的局限性
实现的方式更适合来处理多个线程有共享数据的情况,多个线程可以共享同一个接口实现类的对象 - 相同点:
两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
- 区别
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。 - 联系:
public class Thread implements Runnable {...}
Thread类的有关方法
● void start(): 启动线程,并执行对象的run()方法
● run(): 线程在被调度时执行的操作
● String getName(): 返回线程的名称
● void setName(String name):设置该线程名称
● static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
● static void yield():线程让步,释放当前CPU的执行权
○ 停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
○ 若队列中没有同优先级的线程,忽略此方法
● join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。
○ 低优先级的线程也可以获得执行
● static void sleep(long millis):(指定时间:毫秒)
○ 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
○ 抛出InterruptedException异常
● stop(): 强制线程生命期结束,不推荐使用,已过时
● boolean isAlive():返回boolean,判断线程是否还活着
线程的优先级
- 线程的优先级等级
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 - 涉及的方法
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级 - 说明
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
JDK5.0 新增线程创建方式
新增方式一:Callable 接口实现多线程
- 与使用Runnable相比, Callable功能更强大些
1). 相比run()方法,可以有返回值
2). 方法可以抛出异常
3). 支持泛型的返回值
4). 需要借助FutureTask类,比如获取返回结果 - Future接口
1). 可以对具体Runnable、 Callable任务的执行结果进行取消、查询是否完成、获取结果等。
2). FutrueTask是Futrue接口的唯一的实现类
3). FutureTask 同时实现了Runnable,Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
新增方式二:使用线程池
- JDK 5.0起提供了线程池相关API: ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
1). void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
2). Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
3). void shutdown() :关闭连接池 - Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
1). Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
2). Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
3). Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
4). Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
线程的生命周期
五种状态
- JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

线程的同步
多线程安全问题(卖票过程)
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。


解决办法
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
synchronized的使用方法
- Java对于多线程的安全问题提供了专业的解决方式:同步机制
//同步代码块:
synchronized (对象){
// 需要被同步的代码;
}
- synchronized还可以放在方法声明中,表示整个方法为同步方法。
public synchronized void show (String name){
...
}
- 解决卖票线程安全问题
package com.yao.practice2;
/**
* 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式 存在线程安全问题,待解决
*
* @author YAO
*
*/
class Window2 implements Runnable {
private int ticket = 100; //
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {//可以使用this
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1"); // 为各个线程创建名字
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
同步机制中的锁
- 同步锁机制: 对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源, 使其他任务在其被解锁之前,就无法访问它了, 而在其被解锁之时,另一个任务就可以锁定并使用它了。
- synchronized的锁是什么?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)。
同步代码块:自己指定,很多时候也是指定为this或类名.class 。
- 注意
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this), 同步代码块(指定需谨慎)
- 同步代码块处理继承Thread类的线程安全问题
package com.yao.practice2;
/**
* 例子:创建三个窗口卖票,总票数为100张,继承Thread类 存在线程安全问题,待解决
*
* @author YAO
*
*/
class Window3 extends Thread {
private static int ticket = 100; // 关键字 static的作用
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {//可以使用synchronized (Window3.class) ,类也是对象
if (ticket > 0) {
try {
Thread.sleep(10);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Window3 w2 = new Window3();
Window3 w3 = new Window3();
w1.setName("窗口1"); // 为各个线程创建名字
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
- 同步方法处理实现Runnable的线程安全问题
package com.yao.practice2;
/**
* 例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式 *同步方法处理实现Runnable的线程安全问题
*
* @author YAO
*
*/
class Window4 implements Runnable {
private int ticket = 100; //
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w = new Window4();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1"); // 为各个线程创建名字
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 同步方法处理处理继承Thread类的线程安全问题
package com.yao.practice2;
/**
* 例子:创建三个窗口卖票,总票数为100张,同步方法处理处理继承Thread类的线程安全问题
*
* @author YAO
*
*/
class Window5 extends Thread {
private static int ticket = 100; // 关键字 static的作用
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show() {//锁: Window5.class,类也是对象
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest5 {
public static void main(String[] args) {
Window5 w1 = new Window5();
Window5 w2 = new Window5();
Window5 w3 = new Window5();
w1.setName("窗口1"); // 为各个线程创建名字
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
总结
- 同步方法仍然涉及到同步监视器,只是不需要显示的声明。
- 非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
死锁
- 1
package com.yao.practice2;
public class DeadLock2 {
public static void main(String[] args) {
final StringBuffer s1 = new StringBuffer();//java内部类访问局部变量时局部变量必须声明为final。
final StringBuffer s2 = new StringBuffer();
new Thread() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
- 2
package com.yao.practice2;
class Book {
public synchronized void tell(Painting paint) {
System.out.println("张三对李四说:给画");
paint.get();
}
public synchronized void get() {
System.out.println("张三得到李四的画");
}
}
class Painting {
public synchronized void tell(Book book) {
System.out.println("李四对张三说:给书");
book.get();
}
public synchronized void get() {
System.out.println("李四得到张三的书");
}
}
public class DeadLock implements Runnable {
private Book book = new Book();
private Painting paint = new Painting();
public DeadLock() {
new Thread(this).start();
book.tell(paint);
}
@Override
public void run() {
paint.tell(book);
}
public static void main(String[] args) {
// System.out.println(Thread.currentThread().getName());
new DeadLock();
}
}
@如果某个任务对象调用了f(),对于同一个对象而言,就只能等到f()调用结束后并释放了锁之后,其他任务才能调用f()和g()。对于某个特定对象来说,其所有synchronized方法共享同一个锁,这可以用来防止多个任务同时访问被编码为对象内存。