JAVA SE学习笔记(七):终章:Java程序设计基础笔记(全10万字)

Java程序设计入门

​                                              copyright © 2020 by 宇智波Akali

目录

文章目录


第1章 Java基础语法

1、数据类型​​

1.1 八种基本数据类型

b y t e , s h o r t , i n t , l o n g , f l o a t , d o u b l e , c h a r , b o o l e a n byte, short, int, long, float, double, char, boolean byte,short,int,long,float,double,char,boolean

在Java中的关键字、保留字、标识符等使用方法和C++相似,但是变量:必须初始化

byte类型的取值范围也是[-128, 127]。

long类型和float类型在定义时,值后面要加l或L, f或F。

long a = 3l; float b = 1.22F;

boolean类型不用0或非0的数值来表示,仅由 T r u e / F a l s e True/False True/False表示,这里和其他语言不同。

boolean b1 = true;  boolean b2 = false;

换行输出语句:

System.out.println(str);

输入语句示例 e. g. 1-01

//读入单个数据
Scenner scenner = new Scenner(System.in);
int input = scenner.nextInt(); //String input = scenner.next();
scenner.close();
//一次读入多个数据
Scenner scenner = new Scenner(System.in);
String str = scenner.nextLine();	//一次读入一行字符串
String strs[] = str.split(" "); 	//将上面的字符串按照空格划分为各个单位存在数组中
int a = Integer.parseInt(strs[0]);//通过基本类型的包装类将字符串转为基本数据类型
double b = Double.parseDouble(strs[1]);

1.2引用类型:String

String str = "Hello world!";

​ 引用类型都可以用null作为值,也就是说可以在初始化的时候赋值为null,所以,String可以使用null作为值,此外,String与其他类型还有一个不同点:其值是不变的,这和引用类型在内存中的存储机制有关,后面会有涉及。

int i0 =1;
int i1 = 1;
//以上这种情况会在内存中存储2个1的值

String s0 = "Hello";
String s1 = "Hello";
//因为String是引用类型,所以只存在一个"Hello",变量名去引用"Hello".

String可以用加法进行拼接。

1.3基本数据类型转换

  • char, byte, short三者同级,不可互相转换且<int <long <float < double。

  • 多种数据类型同时计算时统一成最大容量的类型进行计算

  • 而多个相同类型变量运算时,先要转换为相对应的数据类型的默认类型(比如:两个byte类型的变量相加,会把两个byte类型转换为默认的int类型之后再计算,得到的结果是int类型)这种情况适用于变量类型的容量小于默认变量类型

且:当把任何基本类型的值和字符串值进行连接运算时(+),基本数据类型自动转换为字符串类型。

1.3.1强制类型转换
e. g. int k = 7;
       byte a = (byte) k;
//通常字符串不能直接转换为基本类型,但可以通过基本类型对应的包装类来实现:
String a = "43";
int i = Integer.parselnt(a);

boolean类型不可以转换为其他的数据类型

2. 运算符

2.1赋值运算符

支持连续赋值。

思考1

short s = 3;
s=s+2;  ①
s+=2;//①和②有什么区别?

①会报错,因为在运算s+2时会转换为int型,s != int型变量,这里应该手动强制类型转换:
s = (short) s + 2;
②可以正常运行,使用扩展赋值运算符时,会自动将变量类型强制转换为当前类型的变量类型。

思考2

int i = 1;
i *= 0.1;
System.out.println(i);
// 输出 0,因为i = i * 0.1会强制转换为int型,0.1变成0

注意:

如果对负数取模,可以把模数负号忽略不记,如:5%-2=1。
但被模数是负数则不可忽略。此外,取模运算的结果不一定总是整数
int型之间做除法时,只保留整数部分而舍弃小数部分。
“+”除字符串相加功能外,还能把非字符串转换成字符串.例如:
System.out.println("5+5="+5+5); //打印结果是?5+5=55
以下二者的区别:
System.out.println(‘*’ + ‘\t’ +‘*’); 输出:93,为Ascall码,42+9+42=93 System.out.println(“*” + ‘\t’ +‘*’);输出:* *

2.2 逻辑运算符:与&、或|、非!、异或^

异或运算:一个true,一个false则为true 同为true或同为false则为false。

单&时,为逻辑与,左边无论真假,右边都进行运算。
双&时,为短路与,如果左边为真,右边参与运算,如果左边为假,那么右边不参与运算。
“|”和“||”的区别同理,||表示:当左边为真,右边不参与运算。

2.3其他运算符

  • 位运算符:转换为2进制,各个位置进行运算。
  • 三目运算符:(条件表达式)?表达式1:表达式2; 条件表达式为true则运算后的结果为表达式1否则为表达式2。

2.4 运算符优先级

3. 程序流程控制

3.1 顺序结构

按照顺序执行

3.2 分支结构

(1)	if(条件){
		语句;
	}else if(条件){
		语句;
	}else{
		语句;
	}
(2) int i = 1;
switch(i){
	case 1:
		语句;
		break;
	case 2:
		语句;
		break;
	default:
		语句;
		break;
	}
//switch(表达式)中表达式的返回值必须是下述几种类型之一:byte, short, char, int, 枚举,String;
//case子句中的值必须是常量,且其值应该不同,语句块中没有break程序会执行到switch结尾

3.3 循环结构

  • for 循环
  • while 循环
  • do-while 循环
  • 循环嵌套

3.4 特殊流程控制语句

  • break:结束当前所处的整个循环。
  • continue:跳出当前循环,直接进去下一循环。
  • return:并非专门用于结束循环,执行到return语句时,不管出于几层循环内,将结束整个方法。

4. 数组

数组为引用类型,分配空间时要new,传值时传递的是引用。

4.1 一维数组

声明方式:

//仅声明还不能使用,还要初始化,new分配空间。 
type name[]; 
 type[] name1;

数组声明以后在内存中没有分配具体的存储空间,也没有设定数组的长度。

初始化:

  • 动态初始化:数组声明且为数组元素分配空间与赋值的操作分开进行。

    int[] arr = new int[3];		//初始化时(声明时也可以用new)要传入数组长度;如果没有给数组元素赋值,则数组元素会默认初始化,数字类型默认为0,对象类型默认为null。
    arr[0] = 3;
    arr[1] = 9;
    arr[2] = 8;
    
  • 静态初始化:在定义数组的同时就为数组元素分配空间并赋值。

    int a[] = new int[]{ 3, 9, 8};
    int[] a = {3,9,8};
    

4.2 多维数组

声明方式:·

int n[][];

初始化:

  • 动态初始化

    int a[][];
    a = new int[2][3];
    //或者声明和动态初始化同时进行
    int n[][] = new int[2][3];//只声明,没有new分配空间和长度是无法使用的,一维数组也一样
    
  • 静态初始化

    int a[][] = new int [][]{
        {1,2},
        {2,2}
    };
    
  • 让第二维长短不一

    //如果需要初始化第二维长度不一样的二维数组,则可以使用如下的格式:
    int n[][];
    n = new int[2][]; //只初始化第一维的长度
    //分别初始化后续的元素
    n[0] = new int[4];
    n[1] = new int[3];
    //得到
    /*{  
    	{1,2,3,4},
    	{2,3,4}
    }*/
    

5. 源文件布局

5.1 布局格式

[<包声明>]
	[<引用声明>]
	<类声明>

5.2 关键字-package

  1. package语句作为Java源文件的第一条语句,指明该文件中定义的类所在的包。若缺省该语句,则指定为无名包。
  2. 包对应于文件系统的目录,package语句中,用 “.” 来指明包(目录)的层次。
  3. 包通常用小写单词,类名首字母通常大写.

5.3 关键字-import

**作用:**为了使用定义在不同包中的Java类,需用import语句来引入指定包层次下所需要的类或全部类(.*),但是包内的子包中的类不会被引用。

语法格式:import 包名[.子包名…]. <类名 |*>

示例:

import  p1.Test;   //import p1.*;表示引入p1包中的所有类
public class TestPackage{
    public static void main(String args[]){
        Test t = new Test();          //Test类在p1包中定义
        t.display();
    }
}

import语句出现在package语句之后、类定义之前,一个源文件中可包含多个import语句。


第2章 Java 面向对象

1. 什么是面向对象

1.1 面向对象与面向过程

面向过程,强调的是功能行为;面向对象,将功能封装进对象,强调具备了功能的对象。

1.2 面向对象的三大特性

  • 封装性
  • 继承性
  • 多态性

2. 类和对象

2.1类和对象的关系

类 = 汽车设计图 对象 = 实实在在的汽车

面向对象程序设计的重点是类的设计
定义类其实是定义类中的成员(成员变量和成员方法)

2.2 Java类及类的成员

Java的一个类class一般包含:

  1. 属性:即成员变量
  2. 行为:即成员方法(函数)

2.3 类的语法格式

修饰符 class 类名{
	属性声明;
    方法声明;
}

注意:方法中不能再定义方法

e. g. 2-01

public class Person {
    //类的成员变量可以先声明,不用初始化,类成员变量是有默认值
    public String name;			//姓名,String的默认值是null
    private int age;			//年龄,int的默认值是0
    public static String sex = "男";		//静态类变量-后面详解

    /**
     *行为,方法,也叫函数
     *方法的名称如果是多个单词,首个的单词的首字母小写,其他的单词首字母大写,这样就像一个驼峰一样,所	   *以叫驼峰命名法
     */
    public void showName(){	
        System.out.println("姓名:" + name);
        getAge();				//同一个类中,所有的方法可以直接互相调用,不用new去实例化对象
    }

    //有返回值的函数
    public int getAge(){		
        return age;
    }
}

2.4 类的实例化:对象的创建和使用

  • 使用new + 类构造器创建一个新的对象
  • 使用对象名.对象成员的方式访问对象成员(包括对象的属性和方法)

以e. g. 2-01中创建的Person类为例:

e. g. 2-02

public class People {
    public static void main(String[] args){
        //实例化Person类,也就是创建Person对象
        Person person = new Person();
        //声明一个Person类型的变量,变量名person,实例化Person类并且给person赋值,赋值的值就是当前的实例
        person.name = "张三";					//给person对象的name属性赋值
        person.showName();						//对象的方法的调用
        int i = person.getAge();
        System.out.println(i);
        person.age = 11;						//给person对象的age属性赋值
        int a = person.getAge();
        System.out.println(a);
    }
}
//注:如果创建了一个类的多个对象,对于类中定义的属性,每个对象都拥有各自的一套副本,且互不干扰.

2.5 关于对象

2.51 匿名对象
  • 概念: 不定义对象的句柄,而直接调用这个对象的方法,这样的对象叫做匿名对象。
  • 使用情况: 如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象。 经常将匿名对象作为实参传递给一个方法调用。

e. g. 2-03

new Person().shout(); 	//没有给对象命名(定义句柄),直接调用对象中的方法。
2.5.2 在同一个类中的访问机制

​ 类中的方法可以直接访问类中的成员变量。(例外:static方法访问非static的成员变量,编译不通过)

2.5.3 在不同类中的访问机制

​ 先创建要访问类的对象,再用对象访问类中定义的成员。

3. 类的成员之一 :属性

3.1 语法格式

修饰符 类型 属性名 = 初值;

说明: 修饰符private:该属性只能由该类的方法访问,不能通过 对象.属性 的方式调用。
修饰符public:该属性可以被该类以外的方法访问。
类型:任何基本类型,如int、boolean或任何类。

3.2 变量分类

3.2.1 成员变量
  • 实例变量(不以static修饰):

    ​ 就是在类实例化成对象之后才能被使用的变量,如Person中的name(成员变量有默认的init值)。
    ​ 实例变量存在于对象所在的堆内存中。

  • 类变量(以static修饰):

    ​ 这样的变量是静态的,类不需要实例化成对象就可以通过 类名.属性 调用.如Person中的sex。

3.2.2 局部变量
  • 形参(方法签名中定义的变量)
  • 方法局部变量(在方法内定义的变量)
  • 代码块局部变量(在代码块中定义的变量)

注意:

a.局部变量存在于栈内存中,作用的范围结束,变量空间会自动释放.
b.局部变量没有默认初始化值,每次必须显式初始化.
c.局部变量声明时不指定权限修饰符,所以方法内的变量声明不用修饰符

4. 类的成员之二:方法

4.1 语法格式

修饰符 返回值类型 方法名(参数类型 形参1,参数类型 形参2...){
    程序代码
    return 返回值;

说明: 修饰符:public, private, protected等。
返回类型为void,则不用return.关于形参,实参,返回值等不再记录,和C++完全一致

4.2 方法的注意事项

  1. 方法是类或对象行为特征的抽象,也称为函数
  2. Java里的方法不能独立存在,所有的方法必须定义在类里
  3. 方法只有被调用才会被执行
  4. 方法中只能调用方法,不可以在方法内部定义方法
    并且在同一个类中,方法之间可以直接互相调用,所以在方法中调用另一个方法时不需new

4.3 方法的重载

4.3.1 概念

​ 在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可,与形参名无关。

4.3.2 重载代码示例

e. g. 2-04

public int add(int x, int y){
    return x + y;
}

public double add(int x, double y){
    return x + y;
}

public int add(int x, int y, int z){
    return x + y + z;
}
//在主类Text中定义重载函数,在main()中,进行对象实例化,以及方法的调用
Text examp = new Text();
examp.add(1, 2);
examp.add(1, 0.5);
examp.add(1,2,3);
4.3.3 方法重载的特点
  1. 与返回值类型无关,只看参数列表,且参数列表必须不同。
  2. 参数个数或参数类型,参数相同只是顺序不同也可以构成重载,调用时,根据方法参数列表的不同来区别。

4.4 方法之可变个数的形参

4.4.1 定义方法
  • a.使用数组作为形参

传参使用时需要在main函数中new一个数组作为实参传入,数组长度可为 0 → + ∞ 0 \to+ \infty 0+

public void printInfo(String[] args){		//这里的String可以根据需要使用基本数据类型
    for(int i = 0; i < args.length; i++){
        System.out.println(args[i]);
    }
}
  • b.使用Java自带的可变形参表示法

(使用方法与使用数组的形式相同)

e. g. 2-05:

//方法的参数部分有可变形参,需要放在形参声明的最后
public void printInfo1(int d, String... args){
    for(int i = 0; i < args.length; i++){
        System.out.println(args[i]);
    }
}
//使用:(在此之前要在main中实例化一个方法所在的类,命名为p3)
p3.printInfo1(2"lisi", "23", "男");
//或者像使用数组一样
String[] ss2 = new String[]{"北京市xxx","13133334444","152109874523666541"};
p3.printInfo1(ss2);
//不同的是,当参数为空时,使用数组的方法要传入null或空数组,用b方法形参可以空着

4.5 JVM的内存模型图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v31iMMFJ-1587194076057)(Java Record.assets/20200208235224440.png)]

4.6 方法的参数传递

  1. Java里方法的参数传递方式只有一种:值传递

    即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

  2. 参数传递的两种情况:

    • a.如果方法的形参是基本数据类型,那么实参(实际的数据)向形参传递参数时,就是直接传递值,把实参的值复制给形参。
    • b.如果方法的形参是对象,那么实参(实际的对象),向形参传递参数的时,也是把值给形参,这个值是实参在栈内存中的值,也就是引用对象在堆内存中的地址。
    • 基本数据类型都是保存在栈内存中,引用对象在栈内存中保存的是引用对象的地址,那么方法的参数传递是传递值(是变量在栈内存的当中的值)
  3. 参数传递的内存示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sG7r4a8U-1587194076061)(Java Record.assets/20200208235637959.jpg)]

5.类的成员之三:构造器

5.1 语法格式

修饰符  类名 (参数列表) {		//修饰符与类的修饰符一致
    初始化语句;					//可以使用初始化语句和参数列表将类的属性默认初始化或显式初始化
}

5.2 构造器的特征

  1. 它具有与类相同的名称
  2. 它不声明返回值类型。(与声明为void不同)
  3. 不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值

5.3 构造器的作用

​ 创建对象,以及给对象进行初始化。

​ 比如:Order o = new Order();

Person p = new Person(Peter,15);

​ new对象 实际就是调用类的构造器(C++中的构造方法、构造函数在Java中称构造器)。

5.4 构造器的分类

​ 根据参数不同构造器可以分为如下两类:

  • 隐式无参构造器(系统默认提供)
  • 显式定义一个或多个构造器(无参或有参)

注意:

a. Java语言中,每个类都至少有一个构造器
b. 默认构造器的修饰符与所属类的修饰符一致
c. 一旦显式定义了构造器,则系统不再提供默认构造器
d. 一个类可以创建多个重载的构造器
e. 父类的构造器不可被子类继承

5.5 构造器重载

构造器的重载使得对象的创建更加灵活,方便创建各种不同的对象。

e. g. 2-06 :

public class Person{
    //构造器重载,参数列表必须不同
    public Person(String name, int age, Date d) {this(name,age);}
    public Person(String name, int age) {}
    public Person(String name, Date d) {}
    public Person(){}
}

6. 类的成员之四:初始化块(代码块)

01. 作用: 对Java对象进行初始化。

**02. 程序的执行顺序:**声明成员变量的默认值 -> 静态代码块 -> 显式初始化、多个初始化块依次被执行(同级别下按先后顺序执行) -> 构造器再对成员进行赋值操作

**03. 静态代码块:**一个类中初始化块若有修饰符,则只能被static修饰,称为静态代码块(static block ),当类被载入时,类属性的声明和静态代码块先后顺序被执行,且只被执行一次。

04. static块通常用于初始化static (类)属性

示例:e. g. 2-07:

class Person {
    public static int total;
    static {
        total = 100;//为total赋初值 
    }
    …… //其它属性或方法声明
}

05. 非静态代码块:没有static修饰的代码块

​ a. 可以有输出语句。
​ b. 可以对类的属性声明进行初始化操作。
​ c. 可以调用静态和非静态的变量或方法。
​ d. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
​ e. 每次创建对象的时候,都会执行一次,且先于构造器执行。

06. 静态代码块:用static 修饰的代码块

​ a. 可以有输出语句。
​ b. 可以对类的属性声明进行初始化操作。
​ c. 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
​ d. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
​ e. 静态代码块的执行要先于非静态代码块。
​ f. 静态代码块只执行一次。

7. 类的成员之五: 内部类

概念: 在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。

作用: 解决Java不能多重继承的问题(接口也可以),有时只有一个方法想要继承,专门写一个接口显得有点繁琐,可以在类中写一个内部类,用内部类继承该类的方法即可。

a. 内部类可以使用外部类的私有数据,因为它是外部类的成员,同一个类的成员之间可相互访问。而外部类要访问内部类中的成员需要 内部类.成员 或者 内部类对象.成员

b. 内部类可以声明为static,但此时就不能再使用外层类的非static的成员变量。非static的内部类中的成员不能声明为static的,只有在外部类或static的内部类中才可声明static成员

c. 内部类可以声明为abstract类型或final类型 ,因此可以被其它的内部类继承或者终止继承。

代码示例: e. g. 2-08

class A {
    private int s;
    public class B{			//定义内部类
        public void mb() {
            s = 100;     
            System.out.println("在内部类B中s=" + s);
        }  }
    public void ma() {		//定义在外部类中访问内部类的方法
        B i = new B();
        i.mb();
    }  
}

public class Test {	
    public static void main(String args[]){
        A o = new A();			
        o.ma();				//创建外部类对象,通过外部类中的方法访问内部类
    }   
} 

第3章 面向对象的特征

1. 面向对象的特征之一: 封装和隐藏

​ 使用者对类内部定义的属性(对象的成员变量)的直接操作会导致数据的错误、混乱或安全性问题,因此需要对类进行封装和隐藏。

  1. Java中通过将数据声明为私有的(private),再提供公共的(public)方法get_X()和set_X()等方法,实现对该属性的操作,以实现下述目的:

a. 隐藏一个类中不需要对外提供的实现细节;
b. 使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操作;
c. 便于修改,增强代码的可维护性;

  1. 四种访问权限修饰符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNtAsqOE-1587194076062)(Java Record.assets/image-20200319191721641.png)]

  1. 注意:

a. 对于class的权限修饰只可以用public和default(缺省)。
b. public类可以在任意地方被访问。
c. default类只可以被同一个包内部的类访问。
d. 在同一个Java文件中可以写多个class,但是只有一个可以定义为public,其他修饰符空着(default)

2. 面向对象的特征之二: 继 承

​ 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么子类无需再定义这些属性和行为,只要继承父类即可,比如 Person类中的属性和行为在 Student、Teacher等类中也有,后者只需要继承前者即可拥有Person的所有属性、行为。

2.1 定义方法

//定义父类
public class Person {
    public String name;
    public int age;
    public String getInfo() {...}
}
//使用extends关键字定义继承的子类,意即对父类的"扩展".
public class Student extends Person{
    public String school;
}
//Student类继承了父类Person的所有属性和方法,并增加了一个属性school.
//Person中的属性和方法,Student都可以利用.
//如果子类和父类不在同一个包下,子类只能使用父类中public和protected修饰的成员

继承的出现提高了代码的复用性。
继承的出现让类与类之间产生了关系,提供了多态的前提。
不要仅为了获取其他类中某个功能而去继承,继承的类之间应该有逻辑性
子类不能直接访问父类中私有的(private)的成员变量和方法,应通过setter、getter方法访问

注意: 一个子类只能有一个父类,一个父类可以派生出多个子类,Java支持多层继承,但不支持一个子类多个父类这样的多重继承

2.2 方法的重写(override)

(1)概念:在子类中可以根据需要对从父类中继承来的方法进行改造,子类的方法将覆盖父类的方法,称重写

(2)示例:

e. g. 2-07:

public class Person {
    public String name;
    public int age;
    public String getInfo() {
        return "Name: "+ name + "\n" +"age: "+ age;
    }
}
public class Student extends Person {
    public String school;
    public String getInfo() {       //重写方法
        return  "Name: "+ name + "\nage: "+ age 
            + "\nschool: "+ school;
    }
}

(3)方法重写时的注意:

a. 重写方法必须和被重写方法具有相同的方法名称、参数列表和返回值类型。
b. 重写方法不能使用比被重写方法更严格的访问权限。
c. 重写和被重写的方法须同时为static的,或同时为非static的。
d. 子类方法抛出的异常不能大于父类被重写方法的异常

在eclipse中可以使用快捷键 Alt+/ 选择override进行快速重写。

3. 面向对象的特征之三:多态性

3.1 Java中多态性的两种体现

  1. 方法的重载(overload)和重写(overwrite)

    **a. 重载:**本类中允许同名方法存在,体现相同的方法实现不同的逻辑。

    b. 重写: 子类对父类方法的覆盖,子类可以使用和父类相同的方法名,覆盖掉父类的逻辑。

  2. **对象的多态性:**子类的对象可以替代父类的对象使用,可以直接应用在抽象类和接口上。

  3. Java引用变量有两个类型:

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

3.2 对象的多态性

  1. 一个变量只能有一种确定的数据类型,但一个引用类型变量可能指向(引用)多种不同类型的对象,例如:

    Person p = new Person();
    Person e = new Student(); // Person类型的变量e,指向Student类型的对象
    
  2. 子类可看做是特殊的父类,所以父类类型的引用可以指向子类的对象:向上转型(up-casting)。即子类的对象可以给父类类型的变量作为引用。

  3. 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。例如:

    Student m = new Student();
    m.school = “BNU”; 	//合法,Student类有school成员变量
    Person e = new Student(); 
    e.school = “BNU”;	//非法,Person类没有school成员变量
    //属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。
    
  4. 虚拟方法调用

//正常的方法调用
Person p = new Person();
p.getInfo();
Student s = new Student();
s.getInfo();
//虚拟方法调用(多态情况下)
Person e = new Student();
e.getInfo();	//调用Student类的getInfo()方法
//编译时类型和运行时类型,编译时e为Person类型,而方法的调用是在运行时确定的,所以调用的是Student类的getInfo()方法。
//Java的方法是运行在栈内存中的,在运行方法时会动态进栈和出栈--动态绑定.

多态小结:

前提

​ a. 需要存在继承或者实现关系
​ b. 要有覆盖操作

成员方法

​ 编译时:要查看引用变量所属的类中是否有所调用的方法.
​ 运行时:调用实际对象所属的类中的重写方法.
​ 成员方法的多态性,也就是多态绑定,必须在有方法的重写前提下才能进行.

成员变量

​ 不具备多态性,只看引用变量所属的类.


第4章 高级类特性01

1. 关键字-this

作用:

	01.	它在方法内部使用,即表示这个方法所属对象的引用;
	2.	它在构造器内部使用,表示该构造器正在初始化的对象;
            		3.	this表示当前对象,可以调用类的属性、方法和构造器;

使用时机: 当在方法内需要用到调用该方法的对象时,就用this。

使用this调用属性方法代码示例e. g. 4-01:

/**
 * a.当形参与成员变量重名时,如果在方法内部需要使用成员变量,
 * 必须添加this来表明该变量时类成员
 * b.在任意方法内,如果使用当前类的成员变量或成员方法可以在其前面添加this,
 * 增强程序的阅读性
 */
class Person{		// 定义Person类
    private String name ;						
    private int age ;						
    public Person(String name,int age){	
        this.name = name ;   	   	//这里必须用this指针表明name为类的属性,而非形参
        this.age = age ;  }
    public void getInfo(){	
        System.out.println("姓名:" + name) ;
        this.speak();
    }
    public void speak(){
        System.out.println(“年龄:” + this.age);	
    }
}

class Person{					// 定义Person类
    private String name ;		
    private int age ;			
    public Person(){	 		// 无参构造
        System.out.println("新对象实例化") ;
    }
    public Person(String name){
        this();      			// 调用本类中的无参构造方法,且放在构造方法的首行
        this.name = name ;	
    }
    public Person(String name,int age){	
        this(name) ;  			// 调用有一个参数的构造方法,且放在构造方法的首行
        this.age = age;
    }
    public String getInfo(){	
        return "姓名:" + name + ",年龄:" + age ;
    }  
}
/**
			 * c.this可以作为一个类中,构造器相互调用的特殊格式
			 * d.使用this()必须放在构造器的首行
			 * e.使用this调用本类中其他的构造器,保证至少有一个构造器是不用this的。
			 *(实际上就是不能出现构造器自己调用自己)
			 */

2. 关键字-super

作用:

  1. ​ super可用于访问父类中定义的属性,方法,以及父类的构造函数。
  2. ​ a. 尤其当子父类出现同名成员时,可以用super进行区。
    ​ b. super的追溯不仅限于直接父类,还可以调用子类之上的所有父类。
    ​ c. super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识。

使用方法: 使用 super.+父类成员 进行调用。

注意:

a.子类中所有的构造器默认都会访问父类中空参数的构造器

b.当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器,且必须放在构造器的第一行

c.如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错

代码示例: e. g. 4-02:

//定义父类
public class Person {

private String name;
private int age;
private Date birthDate;	

public Person(String name, int age, Date d) {
        this.name = name;
        this.age = age;
        this.birthDate = d;
    }
public Person(String name, int age) {
        this(name, age, null);
	}
public Person(String name, Date d) {
        this(name, 30, d);
 	}
public Person(String name) {
        this(name, 30);
	}

}

//调用父类
public class Student extends Person {
private String school;

public Student(String name, int age, String s) {
          super(name, age);
          school = s;
    }
public Student(String name, String s) {
          super(name);
          school = s;
	}
public Student(String s) { // 编译出错: no super(),系统将调用父类无参数的构造方法。
          school = s;
	}
}

3. this和super的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BOopuUbD-1587194076064)(Java Record.assets/20200213201203822.png)]

4. JavaBean

概念: JavaBean 是一种Java语言写成的可重用组件。所谓 JavaBean,是指符合如下标准的Java类:

​ a. 类是公共的;
​ b. 有一个无参的公共的构造器;
​ c. 有属性,属性一般是私有的,且有对应的get、set方法;

用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

对于eclipse,在创建了类和其中的属性后,可以使用 右键 -> Source -> Generate Getters and Setters.

5. instanceof操作符

作用:x instanceof A 检验x是否为类A的对象,返回值为boolean型。

要求x所属的类与类A必须是子类和父类的关系,否则编译错误。

如果x属于类A的子类B,x instanceof A 值也为true。

6. Object类

概念: Object类是所有Java类的根父类(基类)。在多层继承中处于最高层的父类一定是Object类。

如果在类的声明中未使用extends关键字指明其父类,则默认父类为Object类 :

示例: e. g. 4-03:

public class Person {
    ...
}
//等价于:
public class Person extends Object {
    ...
}

**作用:**可以用于接收作为参数的类。

示例:e. g. 4-04:

method(Object obj){}//可以接收任何类作为其参数
Person o=new Person();  
method(o);

Object类中的方法:

​ Object是所有类的父类,子类可以调用父类的方法,所以Object中的方法在其他所有类中都可以使用 对象.方法()调用。

方法列表:

num方法类型描述
apublic Object()构造构造方法
bpublic boolean equals(Object obj)普通对象比较
cpublic int hashCode()普通取得Hash码
dpublic String toString()普通对象打印时调用

7. 对象类型转换(Casting)

对Java对象的强制类型转换称为造型

a. 从子类到父类的类型转换可以自动进行。
b. 从父类到子类的类型转换必须通过造型(强制类型转换)实现。
c. 无继承关系的引用类型间的转换是非法的。
d. String类是Object类的子类,所以也满足上面的法则。

对象类型转换代码示例:e. g. 4-05:

public class Test{
    public void method(Person e){	 //设Person类中没有getschool()						方法
        // System.out.pritnln(e.getschool());   //非法,编译时错误,因为e在形参传入时是Person类的对象,没有.getschool()方法,应该进行判断和转换

        if(e  instanceof  Student){
            Student me = (Student)e;	//将e强制转换为Student类型
            System.out.pritnln(me.getschool());
        }	    
    }
    public static  void main(Stirng args[]){
        Test t = new Test();
        Student m = new Student();
        t.method(m);
    }
}

8. ==操作符与equals方法

8.1 ==操作符

  1. 基本类型比较值:只要两个变量的值相等,即为true。
  2. 引用类型比较引用(是否指向同一个对象):只有指向同一个对象时(在内存中的地址),==才返回true。
  3. 用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错。

8.2 equals()方法

  1. 所有类都继承了Object,也就获得了equals()方法,还可以对方法进行重写。
  2. 只能比较引用类型,其作用与“==”相同,比较是否指向同一个对象(地址)。
  3. 调用格式:obj1.equals(obj2);
  4. 特例:当用equals()方法进行比较时,对类File、String、Date及包装类来说,因为在这些类中重写了Object类的equals()方法,所以比较的是类型及内容,而不是引用的是否是同一个对象。

9. String对象的创建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ogDjgu9I-1587194076066)(Java Record.assets/20200213202631107.png)]

10. toString()方法

**作用:**toString()方法在Object类中定义,其返回值是String类型,返回类名和它的引用地址。

打印对象时,默认输出对象的内存地址,即等价于输出 对象.toString()

11. 包装类(Wrapper)

11.1 概念及作用

​ 包装类(封装类)是针对八种基本定义相应的引用类型。基本数据类的包装类主要用于基本数据类型与字符串之间的转换。

​ 基本数据类型的实例化转为对应的包装类后,就有了类的特点,就可以调用类中的方法。

11.2 基本数据类型的包装类

基本数据类型包装类
booleanBoolean
byteByte
shortShort
intInteger
longLong
charCharacter
floatFloat
doubleDouble

11.3 基本数据类型包装成包装类(装箱)

//通过包装类的构造器实现:
int i = 500;   Integer t = new Integer(i);
//还可以通过字符串参数构造包装类对象:
Float f = new Float("4.56");

11.4 获得包装类对象中包装的基本类型变量(拆箱)

//调用包装类的.xxxValue()方法:
boolean b = bObj.booleanValue();

JDK 1.5之后,支持自动装箱,自动拆箱。但类型必须匹配。

11.5 使用包装类进行字符串与基本数据类型之间的转换

a. 字符串转换成基本数据类型

//通过包装类的构造器实现:
int i = new Integer(12);
//通过包装类的parseXxx(String s)静态方法:
Float f = Float.parseFloat(12.1);

b. 基本数据类型转换成字符串

//调用字符串重载的valueOf()方法:
String fstr = String.valueOf(2.34f);
//更直接的方式:
String intStr = 5 + “”;

第5章 高级类特性02

1. 关键字-static

1.1 概念及作用

​ 在Java类中,可用static修饰属性、方法、代码块、内部类。

​ 被修饰后的成员具备以下特点

​ a. 随着类的加载而加载;
​ b.优先于对象存在;
​ c.修饰的成员,被所有对象所共享;
​ d.访问权限允许时,可不创建对象,直接被类调用。

1.2 类变量

类变量,即被static修饰的类属性。

a. 类变量不用实例化,直接 类名.属性名 就可以使用,是类的一部分,被所有类的实例化对象共享,也称为静态变量。

b. 类变量的使用要小心,因为只要有一处改动说有对象都会得到变化。

示例代码:e. g. 5-01:

class Person {
    private int id;
    public static int total = 0;	//使用static修饰
    public Person() {				//构造方法,每new一个对象就使total加一并传给id
        total++;
        id = total;
    }
}

1.3 类方法

类方法,即被static修饰的类方法。

a. 没有对象的实例时,可以用**类名.方法名()**的形式访问由static标记的类方法.

b. 在static方法内部只能访问类的static属性,不能访问类的非static属性。

c. 因为不需要实例就可以访问static方法,因此static方法内部不能有this和super。

d. 重载的方法需要同时为static修饰或者非static修饰。

示例代码:e. g. 5-02:

class Person {
    private int id;
    private static int total = 0;
    public static int getTotalPerson() { 
        id++;	//非法
        return total;
    }
    public Person() {
        total++;
        id = total;
    }
}

2. 关键字-final

作用:

a. 在Java中声明类、属性和方法时,可使用关键字final来修饰,表示“最终”。

b. final标记的类不能被继承。提高安全性,提高程序的可读性。

c. final标记的方法不能被子类重写

d. final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。

e. final标记的成员变量必须在声明的同时或在每个构造方法中或代码块中显式赋值,然后才能使用。

f. static final:全局常量

示例代码:e. g. 5-03:

public final class Test{
    public static int totalNumber = 5 ;
    public final int ID;
    public Test(){
        ID = ++totalNumber;  //可在构造方法中给final变量赋值
    }
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.ID);		
        final int I = 10;
        final int J;
        J = 20;
        J = 30;  //非法
    }
}

3. 抽象类和抽象方法

定义: Java允许类设计者指定:超类声明一个方法但不提供实现,该方法的实现由子类提供。这样的方法称为抽象方法。有一个或更多抽象方法的类称为抽象类。

声明: 用abstract关键字来修饰一个类时,将该类声明为抽象类,用abstract来修饰一个方法时,该方法为抽象方法:abstract int abstractMethod( int a );

注意:

a. 含有抽象方法的类必须被声明为抽象类。

b. 抽象类不能被实例化。抽象类是用来作为父类被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,则仍为抽象类。

c. 不能用abstract修饰属性、私有方法、构造器、静态方法、final的方法。

代码示例: e. g. 5-04:

		//Vehicle是一个抽象类,有两个抽象方法。
public abstract class Vehicle{
    int num;
    public abstract double calcFuelEfficiency();	//计算燃料效率的抽象方法
    public abstract double calcTripDistance();	//计算行驶距离的抽象方法
}
public class Truck extends Vehicle{
    public double calcFuelEfficiency(){
        //写出计算卡车的燃料效率的具体方法   
    }
    public double calcTripDistance(){
        //写出计算卡车行驶距离的具体方法   
    }
}

public class RiverBarge extends Vehicle{
    public double calcFuelEfficiency(){
        //写出计算驳船的燃料效率的具体方法  
    }
    public double calcTripDistance(){
        //写出计算驳船行驶距离的具体方法
    }
}
//注意:抽象类不能实例化,即new Vihicle()是非法的.抽象类可以有构造方法,只是不能直接创建抽象类的实例对象而已。

4. 设计模式

4.1 什么是设计模式

概念: 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。

4.2. 单例设计模式

概念: 采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

目的: 实例化对象的创建需要消耗大量的时间和资源时,或者没有必要频繁的new新对象,这种情况就适合用单例设计模式。

4.2.1 饿汉式单例设计

先new好一个对象,有需要时都用这个对象。

示例代码:e. g. 5-05:

public class Single{
    //private的构造器,使得不能在类的外部使用new创建该类的对象
    private Single() {}
    //私有的,静态的Single类型的变量,只能在类的内部访问,并且类创建是就创建该变量
    private static Single onlyone = new Single();
    //getSingle()为static,不用创建对象即可访问
    public static Single getSingle() {
        //通过getSingle()方法返回对象,返回上一行创建的类
        return onlyone;			
    }
}

public class TestSingle{
    public static void main(String args[]) {		
        Single  s1 = Single.getSingle();      //访问静态方法
        Single  s2 = Single.getSingle();
        if (s1==s2){
            System.out.println("s1 is equals to s2!");
        }
    }
}
4.2.2 懒汉式单例设计

最开始对象为null,直到被调用才new一个对象,并且之后都用这个对象。

示例代码:e. g. 5-06:

public class Singleton{
    //1.将构造器私有化,保证在此类的外部,不能调用本类的构造器。
    private Singleton(){}
    //2.创建Singleton类型的static对象
    private static Singleton instance = null;
    //3.设置公共的方法来访问类的实例
    public static Singleton getInstance(){
        //3.1如果类的实例未创建,那些先要创建,然后返回给调用者:本类。因此,需要static 修饰。
        if(instance == null){
            instance = new Singleton();
        }
        //3.2 若有了类的实例,直接返回给调用者。
        return instance;
    }
}

4.3 模板方法设计模式(Template-method)

概念: 抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,在抽象类中给出的抽象方法相当于文章的各章节标题(模式),子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。

目的: 当功能内部一部分实现是确定,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。

代码示例:e. g. 5-07:

//程序计时
abstract class Template{
    public final void getTime(){
        long start = System.currentTimeMillis();
        code();									
        long end = System.currentTimeMillis();
        System.out.println("执行时间是:"+(end - start));
    }
    public abstract void code();//将code();程序留白,在子类中根据需要再编写
}
class SubTemplate extends Template{
    public void code(){
        for(int i = 0;i<10000;i++){
            System.out.println(i);
        } 
    } 
}

4.4 工厂方法(Factory-method)

​ FactoryMethod模式是设计模式中应用最为广泛的模式,在面向对象的编程中,对象的创建工作非常简单,对象的创建时机却很重要。通过工厂把new对象隔离,通过产品的接口可以接受不同实际产品的实现类,实例的类名发生改变并不影响其他人对类的调用。

5. 接 口(interface)

5.1 概念

​ 接口(interface)是抽象方法和常量值的定义的集合,从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量public static final)和方法的定义,而没有变量和方法的实现。

5.2 作用01

​ 有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承,只支持多层继承。有了接口,可以让子类实现多个接口,就可以得到多重继承的效果。接口的主要用途就是被实现类实现。(面向接口编程)

5.3 接口的定义与实现

  1. 用interface来定义,接口中的所有成员变量都默认是由public static final修饰的。接口中的所有方法都默认是由public abstract修饰的。
  2. 接口没有构造器,接口采用多层继承机制。
  3. 一个类可以实现多个接口,接口也可以继承其它接口
  4. 与继承关系类似,接口与实现类之间存在多态性,即接口可以接收一个类的对象,即将对象转为接口类型,对象中只包含接口中定义的方法。

定义接口 代码示例:e. g. 5-08:

//定义接口
public interface InterfaceA{
    int ID = 1;
    void start();
    public void run();
    void stop();
}
//或者按照标准写法
public interface InterfaceA{
    public static final int ID = 1;
    public abstract void start();
    public abstract void run();
    public abstract void stop();
}

实现接口 代码示例:e. g. 5-09:

//在类中实现接口
class SubClass implements InterfaceA{
    //实现接口中的抽象方法
}
//实现接口的类中必须提供接口中所有方法的具体实现内容,方可实例化。否则,仍为抽象类。

含有接口和继承的类的定义语法:先写extends,后写implements

< modifier> class < name> [extends < superclass>]
    [implements < interface> [,< interface>]* ] {
    < declarations>*
}

example: 用Java描述一个会唱歌做饭的老师:

如果使用继承,则需要多层继承:

​ 显然这很不方便,如果单个功能需要修改则在子类中都需要修改。并且上述几个类之间功能独立,并没有明显的联系,使用继承太过牵强。

​ 这时候可以定义Teacher类继承Person类,定义Sing、Cooking接口,并让Teacher类实现这些接口。

代码示例:e. g. 5-10:

interface Sing { public void sing();}
interface Cooking {public void fry();}
abstract class People{
    String name;
    int age;
    public int eat(){}
} 
class Teacher extends People implements Sing,Cooking{
		public void run() {……}
		public double swim()  {……}
		public int eat() {……}
}

5.4 作用02

​ 当抽象父类中需要新增抽象方法时,子类中必须实现该新增方法,否则子类就应改为抽象类,同理,子类的子类…都需要作出改变,这样代价大,不方便。这时可以将新增的抽象方法写为一个接口,根据需要在对应的子类中实现接口即可。

代码示例:e. g. 5-11:

interface Runner { public void run();}
interface Swimmer {public double swim();}
class Creator{public int eat(){}} 
class Man extends Creator implements Runner ,Swimmer{
    public void run() {……}
    public double swim()  {……}
    public int eat() {……}
}

注意: 如果实现接口的类中没有实现接口中的全部方法,必须将此类定义为抽象类 。

5.5 接口的多态以及接口的应用

接口只是一个规范,所以里面的方法都是空的。
假如我开了一个宠物粮店,声明所有宠物都可以来我这里买粮食,这就相当于一个接口,

public interface PetRestaurant {
 	public void buy();
}

当一只狗看到了,知道自己是宠物,所以它去实现这个接口

public class DogPet implements PetRestaurant {
 	@Override
 	public void buy() {
  		System.out.println("我是狗,我要买狗粮");
 	}
}

当一只猫看到了,知道自己也是宠物,所以也去实现这个接口

public class CatPet implements PetRestaurant {
 	@Override
    public void buy() {
  		System.out.println("我是猫,我要买猫粮");
    }
}

当狗和猫来我的店之前,我是不知道他们到底是什么,但是当他们来到我的店,我就知道一个要猫粮食,一个要狗粮食。因为他们都实现了 我这个接口,都可以买。下面这个类相当于一个接待顾客的类,即店小二,他接待所有实现了我这个宠物店接口的动物,传进来一个PetRestaurant 类型的宠物,注意,这个PetRestaurant 是接口

public class test {
 	public void buy(PetRestaurant pet){
  		pet.buy();
 	}
}

好了,这个时候我这个老板出现了,我可以给他们卖粮食了,相当于测试类

public class Tests {
 	public static void main(String[] args) {
  		PetRestaurant dog = new DogPet();  //实例化一个狗,相当于把狗顾客实例化
  		PetRestaurant cat = new CatPet();//实例化一个猫,相当于把猫顾客实例化

 	test t = new test();  //实例化一个店小二
  	t.buy(cat);  //把猫交给店小二
  	t.buy(dog); //把狗交给店小二
 	}
}
//这样运行的结果是
//我是猫,我要买猫粮
//我是狗,我要买狗娘

你知道吗,整个过程我这个店主其实根本不知道来的到底是猫是狗还是其他什么,我只要有一个店小二,把这些来的不知什么动物都全部交给店小二,店小二就知道怎么去卖了,因为这些狗啊猫啊都实现了我这个宠物店的接口,而店小二就负责接待所有实现了我这个接口的动物。这就有一个好处,假如明天来了一头小猪,只要它实现了我这个接口,我只管交给店小二处理就OK了,我这个店小二根本不需要变化,我这个店主也只需要实例化一下这个动物就OK
你想,假如没有接口,会怎么办,来一个猫,我要去创造一个猫,还要实例化,来一只狗,我要创建一只狗,同样要实例化,还要配备专门的店小二去接待,就会相当麻烦。

5.6 接口的继承

接口也可以继承另一个接口,使用extends关键字。

interface MyInterface{
		public static final String s=“MyInterface”;
		public void absM1();
}
interface SubInterface extends MyInterface{
		public void absM2();
}
public class SubAdapter implements SubInterface{
		public void absM1(){System.out.println(“absM1”);}
		public void absM2(){System.out.println(“absM2”);}
}
//实现类SubAdapter必须给出接口SubInterface以及父接口MyInterface中所有方法的实现。

抽象类和接口:

​ 抽象类是对一类事物的高度抽象,其中既有属性也有抽象,方法;接口是对方法的抽象,不能在接口中定义常规属性,即一系列动作的抽象。所以,当需要对一类事物的抽象时,就用抽象类,以形成父类;当需要对一系列动作进行抽象时,就使用接口,在需要这些动作的类中实现这些接口。

面向对象总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iN1qvH52-1587194076067)(Java Record.assets/image-20200322220926716.png)]


第六章 异 常

1. 异常的分类

对于遇到的错误,一般有两种解决方法:

  1. 遇到错误就终止程序的运行。

  2. 由程序员在编写程序时,就考虑到错误的检测、错误消息的提示,以及错误的处理。

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

Error: JVM系统内部错误、资源耗尽等严重情况.

Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,例如:空指针访问、试图读取不存在的文件、网络连接中断等。

程序员通常只能处理Exception,而对Error无能为力。

java异常类层次

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vHw0nca-1587194076068)(Java Record.assets/image-20200322222022155.png)]

常见异常

  1. RuntimeException
    • 错误的类型转换
    • 数组下标越界
    • 空指针访问
  2. IOExeption
    • 从一个不存在的文件中读取数据
    • 越过文件结尾继续读取EOFException
    • 连接一个不存在的URL

2. 异常处理机制

​ 在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的分支会导致程序的代码加长,可读性差。因此采用异常机制:将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁,并易于维护。

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

  • Java程序的执行过程中如出现异常,会自动生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。

  • 如果一个方法内抛出异常,该异常会被抛到调用方法中。如果异常没有在调用方法中处理,它继续被抛给这个调用方法的调用者。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。

  • 如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。

异常处理格式:

//异常处理是通过try-catch-finally语句实现的。 
try{
    ......	//可能产生异常的代码
}
catch( ExceptionName1 e1 )
{
    ......	//当产生ExceptionName1型异常时的处置措施,也可使用所有异常的父类Exception
}
catch( ExceptionName2 e2 )
{
    ...... 	//当产生ExceptionName2型异常时的处置措施
        //与其它对象一样,可以访问一个异常对象的成员变量或调用它的方法。
        //getMessage() 方法,用来得到有关异常事件的信息
        //printStackTrace()用来跟踪异常事件发生时执行堆栈的内容。

}  
[ finally{
    ......	//无条件执行的语句
        //不论在try、catch代码块中是否发生了异常事件,finally块中的语句都会被执行。
        //finally语句是可选的

}  ]

还可进行抛出异常声明重写方法声明抛出异常人工抛出异常创建用户自定义异常类

声明抛出异常是Java中处理异常的第二种方式

如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显式地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。

在方法声明中用 throws 子句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

这里只关注异常的捕获,其他处理方式不详细了解。

3. 异常处理代码示例(捕获)

数组越界

e. g. 6-01:

public class Test01{
	public static void main(String[] args){
		String friends[] = {"Lisa","billy","Nessy"};
      	try{
		    for(int i=0;i<5;i++) {
           	System.out.println(friends[i]);
           }
      	}catch(ArrayIndexOutOfBoundsException e){//异常处理
           System.out.println("index err");
      	}
      	System.out.println("\nthis is the end");
  }
}

运行结果:

Lisa billy Nessy
index err this is the end


第七章 Java集合

1.集合概述

01. Java集合类存放于 java.util 包中,是一个用来存放对象的容器。
a.集合只能存放对象。比如你存一个 int 型数据 1放入集合中,其实它是自动转换成 Integer 类后存入的,Java中每一种基本类型都有对应的引用类型。
b.集合存放的是多个对象的引用,对象本身还是放在堆内存中。
c.集合可以存放不同类型,不限数量的数据类型。

02 .Java 集合可分为 Set、List 和 Map 三种大体系
a. Set:无序、不可重复的集合
b. List:有序,可重复的集合
c. Map:具有映射关系的集合

2. Set

2.1. HashSet

01 . HashSet 是 Set 接口的典型实现,我们大多数时候说的set集合指的都是HashSet,按 Hash 算法来存储集合中的元素,因此具有很好的存取和查找性能

02 .HashSet 具有以下特点:
a.不能保证元素的排列顺序
b.不可重复
HashSet 不是线程安全的
集合元素可以使 null

遍历方法:

03 .使用 Iterator 接口遍历集合元素
a. Iterator 接口主要用于遍历 Collection 集合中的元素,Iterator 对象也被称为迭代器

04 .使用 for each 循环遍历集合元素

代码案例:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set
public class Test_HashSet {
	public static void main(String[] args) {
		Set set = new HashSet();
		set.add(1);						//向Hash集合中添加元素
		set.add("a");
		
		System.out.println(set);
		
		set.remove(1);					//移除元素
		
		System.out.println(set);
		
		System.out.println(set.contains(1)); //判断元素是否存在
		
		set.clear();		//清空集合
		
		System.out.println(set);
		
		set.add("a");
		set.add("b");
		set.add("c");
		set.add("d");
		System.out.println(set);
		//使用迭代器接口遍历集合
		Iterator it = set.iterator();
		
		while(it.hasNext()){
			System.out.println(it.next());
		}
		
		//for each迭代集合,推荐使用
		for(Object obj : set){//意思是把set的每个值取出来,赋值给obj,直到循环set的所有值
			System.out.println(obj);
		}
		
		System.out.println(set.size());	//获取集合元素个数
		
		//如果想让集合只能存同样类型的对象,使用泛型
		Set<String> set1 = new HashSet<String>();//比如指定String为集合的泛型,那么这个集合不能存String之外的数据
		set.add("abc");
		System.out.println(set);
		
		//Set set = new HashSet();  等价于 Set<Object> set = new HashSet<Object>();
	}
	
}

2.2. TreeSet

01 . TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。TreeSet 支持两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。

02.定制排序:如果需要实现定制排序,则需要在创建 TreeSet 集合对象时,提供一个 Comparator 接口的实现类对象。由该 Comparator 对象负责集合元素的排序逻辑。

代码示例:

import java.util.Comparator;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
public class Test_TreeSet {
	public static void main(String[] args) {
//		Set<Integer> set = new TreeSet<Integer>();	//TreeSet必须放入相同类型的对象,或直接用泛型限定
//		TreeSet自然排序
//		set.add(5);
//		set.add(2);
//		set.add(4);
//		set.add(3);
//		System.out.println(set);
//		set.remove(5);
//		set.contains(3);
//		set.clear();
//		
//		//使用迭代器遍历集合
//		Iterator<Integer> it = set.iterator();
//		while(it.hasNext()){
//			System.out.println(it.next());
//		}
//		//for each迭代集合,推荐
//		for(Integer i : set){
//			System.out.println(i);
//		}
		
		Person p1 = new Person("张三",23);
		Person p2 = new Person("lisi",20);
		Person p3 = new Person("wangwu",16);
		Person p4 = new Person("Lucy",29);
		
		Set<Person> set = new TreeSet<Person>(new Person()); 
        //这里传入构造,因为在TreeSet源码里构造方法能传一个Comparator比较器的参数,按传参的比较器的定义来排序
		set.add(p1);
		set.add(p2);
		set.add(p3);
		set.add(p4);
		
		for(Person p : set){
			System.out.println(p.name+"  "+ p.age);
		}
		
	}
}
//定制排序
class Person implements Comparator<Person>{	//把Person对象存到TreeSet中并且按照年龄排序
	int age;
	String name;
	
	public Person(){
		
	}
	public Person(String name, int age){
		this.name = name;
		this.age = age;
	}
	
	@Override
	public int compare(Person o1, Person o2) {//重写Comparator中的排序,按年龄正序排列
		if(o1.age > o2.age){
			return 1;
		}else if(o1.age < o2.age){
			return -1;
		}else{
			return 0;
		}
	}
}

3. List与ArrayList

01 . List 代表一个元素有序、且可重复的集合,集合中的每个元素都有其对应的顺序索引
List 允许使用重复元素,可以通过索引来访问指定位置的集合元素。
List 默认按元素的添加顺序设置元素的索引。

代码示例:

import java.util.ArrayList;
import java.util.List;

public class Test_ArrayList {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("b");	//下标为0
		list.add("c");	//下标为1
		list.add("d");	//下标为2
		list.add("a");	//下标为3
		list.add("d");	//允许重复
		System.out.println(list);
		System.out.println(list.get(2));
		
		list.add(1, "f");	//在指定索引下标位置插入
		System.out.println(list);
		
		//在指定下标插入集合,集合数据类型必须与与其一致
		List<String> list02 = new ArrayList<String>();
		list02.add("123");
		list02.add("456");
		
		list.addAll(2, list02);	//插入集合
		System.out.println(list);
		
		System.out.println(list.indexOf("d"));	//查找元素第一次出现的下标,没有则输出-1
		System.out.println(list.lastIndexOf("d"));	//查找元素最后一次出现的下标
		
		list.remove(2);	//根据索引移除元素
		list.set(3, "ff");	//根据指定的索引修改值
		System.out.println(list);
		
		System.out.println(list.subList(2, 5));	//list.subList(a, b),截取a到b-1的元素
		System.out.println(list.size());
		
	}
}

4. Map

01 . Map 用于保存具有映射关系的数据,因此 Map 集合里保存着两组值,一组值用于保存 Map 里的 Key,另外一组用于保存 Map 里的 Value。

02 . Map 中的 key 和 value 都可以是任何引用类型的数据,Map中的Key不允许重复。

03 . Map常用的两种为HashMap和TreeMap。

代码示例:

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

public class Test_HashMap {
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("a", 1);	//使用.put();方法添加元素
		map.put("b", 4);
		map.put("c", 4);
		map.put("d", 8);
		System.out.println(map);
		
		System.out.println(map.get("b"));	//根据key取值
		map.remove("c");
		System.out.println(map);			//根据key移除元素
		System.out.println(map.size());		//映射的个数
		System.out.println(map.containsKey("f"));	//判断当前的集合是否包含key
		System.out.println(map.containsValue(4));	//判断当前的集合是否包含value
		
//		map.clear();								//清空集合
		
		//遍历map
		map.keySet();				//可以获取所有的key集合
		map.values();				//可以获取所有的values集合
		System.out.println(map.keySet());
		System.out.println(map.values());
	
		//1.可以通过map.keySet()来遍历
		Set<String> keys = map.keySet();
		
		for(String key : keys){
			System.out.println("key:"+ key + ", value:"+ map.get(key));
		}
		
		//2.通过map.entrySet();遍历
		Set<Entry<String, Integer>> entrys = map.entrySet();
		for(Entry<String, Integer> en : entrys){
			System.out.println("key:" + en.getKey() +", value:" +en.getValue());
		}
		
		//TreeMap可以按自然排序(字典排序),也可自定义排序(未举例)
		Map<Integer, String> tree_map = new TreeMap();
		tree_map.put(3, "a");
		tree_map.put(1, "b");
		tree_map.put(4, "c");
		tree_map.put(2, "d");
		System.out.println(tree_map);
	}
}

5. 操作集合的工具类:Collections

Collections 是一个操作 Set、List 和 Map 等集合的工具类

排序操作:

​ reverse(List):反转 List 中元素的顺序
​ shuffle(List):对 List 集合元素进行随机排序
​ sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
​ sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
​ swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

查找和替换:

Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素

Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素

Object min(Collection)

Object min(Collection,Comparator)

int frequency(Collection,Object):返回指定集合中指定元素的出现次数

boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值

因为Collections类是匿名内部类,可以直接调用,不用(不能)实例化,例如

在已有list的情况下,直接使用如下指令,可翻转list

Collections.reverse(list);


第八章 泛 型

1. 为什么需要泛型(Generic)

​ 泛型,解决数据类型的安全性问题,其主要原理是在类、接口、方法声明时通过一个标识表示其中某个属性的类型或者是某个方法的返回值及参数类型。这样在声明或实例化时只要指定好需要的具体的类型即可。

​ Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。Java中的泛型,只在编译阶段有效,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

​ 简单说,泛型就是一个类型限定的方法,可以在类、方法、接口中使用泛型,以便限定参数类型,同时,在未知参数类型时,泛型可以根据接收的参数,自动确认返回类型。

2. 泛型类

指定类的数据类型,集合也同理。
//定义泛型类,在类名后加上<泛型名>,这里的泛型名可以随意设置,只是个代号,一般大写T,意为Type
class A<T>{
	private T key;
	
	public void setKey(T key){
		this.key = key;
	}
	
	public T getKey(){
		return this.key;
	}
}

对象实例化时不指定泛型,默认为:Object类并且泛型不同的引用不能相互赋值

3. 泛型接口

在声明类的时候,需将泛型的声明也一起加到类中

//定义一个泛型接口
interface Generator<T> {
T next();
}
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型

class FruitGenerator implements Generator<String> {
@Override
public String next() {
// TODO Auto-generated method stub
return null;
	}
}

4. 泛型方法

方法也可以被泛型化,不管此时定义在其中的类是不是泛型化的。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。

public class DAO {
    public <E>  void show(E e){
        System.out.println(e.toString());
    }
    public <T> T show1(T t){
        return t;
    }
}

泛型方法与可变参数

/**
 * 泛型方法与可变参数
 * @param args
 */
public <T> void printMsg( T... args){
    for(T t : args){
       System.out.println("泛型测试 ,t is " + t);
    }
}

静态方法无法访问类上定义的泛型,即使静态方法要使用泛型类中已经声明过的泛型也不可以

如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上

5. 通配符

/**
 * 不确定集合中的元素具体的数据类型
 * 使用?表示所有类型
 * @param list
 */
public void test(List<?> list){
System.out.println(list);
}

有限制的通配符:

举例:

<? extends Person> 限制区间: (无穷小 , Person]

只允许泛型为Person及Person子类的引用调用

<? super Person> 限制区间: [Person , 无穷大)

只允许泛型为Person及Person父类的引用调用

**<? extends Comparable> **

只允许泛型为实 现Comparable接口的实现类的引用调用


第九章 枚举和注解

1. 枚举类

枚举类概述:

在某些情况下,一个类的对象是有限而且固定的。例如季节类,只能有 4 个对象。如果手动实现枚举类,代码量很大,如下:

枚举类和普通类的区别

使用 enum 定义的枚举类默认继承了 java.lang.Enum

•枚举类的构造器只能使用 private 访问控制符

•枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾). 列出的实例系统会自动添加 public static final 修饰

所有的枚举类都提供了一个 values 方法, 该方法可以很方便地遍历所有的枚举值

JDK 1.5 中可以在 switch 表达式中使用枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定

•若枚举只有一个成员, 则可以作为一种单子模式的实现方式

枚举类对象的属性不应允许被改动, 所以应该使用 private final 修饰

•枚举类使用 private final 修饰的属性应该在构造器中为其赋值

•若枚举类显式的定义了带参数的构造器, 则在列出枚举值时也必须对应的传入参数

枚举代码示例:

public class Test {
	public static void main(String[] args) {
		//Season.SPRING,这段执行就是获取一个Season的对象
		Season spring = Season.SPRING;
		spring.showInfo();
		
		Season summer = Season.SUMMER;
		summer.showInfo();
		
		Season spring1 = Season.SPRING;
		//每次执行Season.SPRING获得是相同的对象,枚举类中的每个枚举都是单例模式的
		System.out.println(spring.equals(spring1));
		spring1.test();
		
	}
}
//枚举也可以实现接口
enum Season implements ITest{
	SPRING("春天","春暖花开"),//此处相当于在调用有参的私有构造private Season(String name,String desc)
	SUMMER("夏天","炎炎夏日"),
	AUTUMN("秋天","秋高气爽"),
	WINTER("冬天","寒风凛冽");
	
	private final String name;
	private final String desc;
	
	private Season(String name,String desc){
		this.name = name;
		this.desc = desc;
	}
	
	public void showInfo(){
		System.out.println(this.name + ": " + this.desc);
	}

	@Override
	public void test() {
		System.out.println("这是实现的ITest接口的test方法");
	}
}
//接口
interface ITest{
	void test();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9vYUbHnI-1587194076069)(Java Record.assets/image-20200410182710977.png)]

2. 注解

Annotation(注解) 概述: 注解其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理. 通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息.

三个基本注解:

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

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

•@SuppressWarnings: 抑制编译器警告.

**自定义注解:**使用 @interface 关键字自定义注解类(了解即可)。


第十章 IO 流

1. 文件流

1.1 File 类

File类:是文件和目录路径名的抽象表示形式。File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

File类的常用方法

在这里插入图片描述

File f = new File("D:/test/abc/testFile.txt");	 	//以文件或者目录路径为参数创建File类

1.2 Java IO 原理

Java程序中,对于数据的输入/输出操作以”流(stream)” 的方式进行。

**输入input:**读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。

**输出output:**将程序(内存)数据输出到磁盘、光盘等存储设备中。

流的分类:

根据数据单位分为:字节流(8 bit),字符流(16 bit)

根据数据流向分为:输入流、输出流

流类。

1.3 文件字节流

对流的处理常出现错误,通常try-catch或者throws

1.3.1 文件字节输入流
/**
	 * 文件字节输入流FileInputStream
	 *  在读取文件时,必须保证该文件已存在,否则出异常
	 */
public static void testFileInputStream(){
    try {
        FileInputStream in = new FileInputStream("D:/test/abc/tt1.txt");

        byte[] b = new byte[1024];//设置一个byte数组接收读取的文件的内容

        int len = 0;//设置一个读取数据的长度

        //in.read方法有一个返回值,返回值是读取的数据的长度,如果读取到最后一个数据,还会向后读一个,这个时候返回值是-1.
        //也就意味着当in.read的返回值是-1的时候整个文件就读取完毕了
        
        while((len = in.read(b)) != -1){
            System.out.println(new String(b,0,len));
            //new String(b,0,len),参数1是缓冲数据的数组,参数2是从数组的那个位置开始转化字符串,参数3是总共转化几个字节
        }

        in.close();//注意,流在使用完毕之后一段要关闭
    } catch (Exception e) {
        e.printStackTrace();
    }
}
1.3.2 文件字节输出流
/**文件字节输出流FileOutputStream
    * 在写入一个文件时,如果目录下有同名文件将被覆盖
    */
    public static void testFileOutputStream(){
    try {
        FileOutputStream out = new FileOutputStream("D:/test/abc/tt4.txt");//指定向tt4输出数据(没有该文件则自动创建。)
        String str = "knsasjadkajsdkjsa";
        out.write(str.getBytes());//把数据写到内存
        out.flush();//把内存中的数据刷写到硬盘
        out.close();//关闭流

    } catch (Exception e) {
        e.printStackTrace();
    }
}
1.3.3 使用文件字节流拷贝文件
/**
	 * 复制文件到指定位置
	 * @param inPath 源文件路径
	 * @param outPanth 复制到的文件夹位置
	 */
public static void copyFile(String inPath, String outPanth){
    try {
        FileInputStream in = new FileInputStream(inPath);//读取的源文件

        FileOutputStream out = new FileOutputStream(outPanth);//复制到哪里

        byte[] b = new byte[100];

        int len = 0;

        while((len = in.read(b)) != -1){
            out.write(b, 0, len);//参数1是写的缓冲数组,参数2是从数组的那个位置开始,参数3是获取的数组的总长度
        }

        out.flush();//把写到内存的数据刷到硬盘
        out.close();
        in.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.4 文件字符流

文件字符流的方法调用和文件字节流只有很少的差别。

1.4.1 文件字符输入流
/**
	 * 文件字符输入流FileReader
	 *  在读取文件时,必须保证该文件已存在,否则出异常
	 * @param inPath
	 */
public static void testFileReader(String inPath){
    try {
        FileReader fr = new FileReader(inPath);//创建文件字符输入流的对象

        char[] c = new char[10];//使用char数组临时存数据,是和字节流的唯一区别

        int len = 0;//定义一个输入流的读取长度

        while((len = fr.read(c)) != -1){
            System.out.println(new String(c, 0, len));
        }

        fr.close();//关闭流

    } catch (Exception e) {
        e.printStackTrace();
    }
}
1.4.2 文件字符输出流
/**
	 * 文件字符输出流FileWriter
	 * 在写入一个文件时,如果目录下有同名文件将被覆盖
	 * @param text 输出的内容
	 * @param outPath 输出的文件
	 */
public static void testFileWriter(String text,String outPath){
    try {
        FileWriter fw = new FileWriter(outPath);
        fw.write(text);//写到内存中
        fw.flush();//把内存的数据刷到硬盘
        fw.close();//关闭流

    } catch (Exception e) {
        e.printStackTrace();
    }
}
1.4.3 使用文件字符流拷贝文件
/**
	 * 字符流完成拷贝文件,字符流只适合操作内容是字符文件
	 * @param inPaht
	 * @param outPath
	 */
public static void copyFile(String inPaht, String outPath){
    try {
        FileReader fr = new FileReader(inPaht);
        FileWriter fw = new FileWriter(outPath);

        char[] c = new char[100];

        int len = 0;

        while((len = fr.read(c)) != -1){//读取数据
            fw.write(c,0,len);//写数据到内存
        }

        fw.flush();

        fw.close();
        fr.close();


    } catch (Exception e) {
        e.printStackTrace();
    }
}
}

1.5 文件字节流和文件字符流的区别

​ 字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲等文件,字节流更加合适。如果是关系到中文(文本)的,用字符流好点.字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串。 字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。

2. 处理流

2.1 处理流之一:缓冲流

​ 为了提高数据读写的速度,Java API 提供了带缓冲功能的流类,缓冲流是把数据缓冲到内存中,对读写的数据提供了缓冲的功能,提高了读写的效率,速率大约是不使用缓冲的75000倍。缓冲流对应文件字节流文件字符流两种数据操作单位,分为:

BufferedInputStream BufferedOutputStream 缓冲字节输入、输出流

BufferedReader BufferedWriter 缓冲字符输入、输出流

2.1.1 缓冲字节输入流
/**
	 * 缓冲字节输入流
	 * BufferedInputStream
	 * @throws Exception 
	 */
public static void testBufferedInputStream() throws Exception{
    //文件字节输入流对象
    FileInputStream in = new 				FileInputStream("D:\\testdemo\\demo\\src\\day13\\tt.txt");

    //把文件字节输入流放到缓冲字节输入流对象
    BufferedInputStream br = new BufferedInputStream(in);

    byte[] b = new byte[10];

    int len = 0;

    while((len = br.read(b)) != -1){
        System.out.println(new String(b,0,len));
    }

    //关闭流的时候,本着一个最晚开的最早关,依次关
    br.close();
    in.close();
}
2.1.2 缓冲字节输出流
/**
	 * 缓冲字节输出流
	 * BufferedOutputStream
	 */
public static void testBufferedOutputStream() throws Exception{
    //创建字节输出流对象
    FileOutputStream out = new FileOutputStream("D:\\testdemo\\demo\\src\\day13\\tt1.txt");

    //把字节输出流对象放到缓冲字节输出流中
    BufferedOutputStream bo = new BufferedOutputStream(out);

    String s = "hello world";

    bo.write(s.getBytes());//写到内存中

    bo.flush();//刷到硬盘上

    //关闭流的时候,本着一个最晚开的最早关,依次关
    bo.close();
    out.close();

}
2.1.3 使用缓冲字节流拷贝文件
/**
	 * 缓冲流实现文件的复制
	 */
public static void copyFile() throws Exception{
    //缓冲输入流
    BufferedInputStream br = new BufferedInputStream(new FileInputStream("D:\\testdemo\\demo\\src\\day13\\tt1.txt"));

    //缓冲输出流
    BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream("D:\\testdemo\\demo\\src\\day13\\tt2.txt"));

    byte[] b = new byte[1024];

    int len = 0;//设置一个没出读取到的数据的长度,直到br.read方法执行到最后(比如说文件中只有hello world,执行到最后一个就读取d的后面,这个时候返回值就是-1)

    while((len = br.read(b)) != -1){
        bo.write(b, 0, len);//写到内存
    }

    bo.flush();//刷到硬盘

    bo.close();
    br.close();
}
2.1.4 缓冲字符输入流

略,和字符流只有char数组和类名不同。

2.1.5 缓冲字符输出流

略,和字符流只有char数组和类名不同。

2.1.6 使用缓冲字符流拷贝文件

略,和字符流只有char数组和类名不同。

2.2 处理流之二: 转换流

​ 字符流方式的输入输出操作是建立在字符集的基础上的。
常见字符集

  • ASCII
  • ANSI(GB2321/BIG5/ISO-8859-2/…)
  • Unicode(UTF-8/UTF16BE/UTF16LE/…)
  • Java字符基于Unicode字符集

​ 所有的文件都是有编码格式,对于我们来说,TXT 和java文件一般来讲有三种编码,ISO8859-1,西欧编码,是纯粹英文编码,不适应汉字,GBK 和 UTF-8,这两编码是适用于中文和英文,我们一般使用UTF-8包含较多汉字,所以加载速度较慢,一般网页选择GBK编码。

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

2.2.1 转换字节输入流为字符输入流
/**
	 * 转换字节输入流为字符输入流
	 * 注意,在转换字符流的时候,设置的字符集编码要与读取的文件的数据的编码一致
	 * 不然就会出现乱码
	 * InputStreamReader
	 */
public static void testInputStreamReader() throws Exception{
    FileInputStream fs = new FileInputStream("D:\\testdemo\\demo\\src\\day13\\tt5.txt");

    //把字节流转换为字符流
    InputStreamReader in = new InputStreamReader(fs,"GBK");//参数1是字节流,参数2是编码
    //InputStreamReader in = new InputStreamReader(fs,"UTF-8");//参数1是字节流,参数2是编码,不统一则会乱码。

    char[] c = new char[100];
    int len = 0;

    while((len = in.read(c)) != -1){
        System.out.println(new String(c,0,len));
    }

    in.close();
    fs.close();
}
2.2.2 转换字节输出流为字符输出流
/**
	 * 转换字节输出流为字符输出流
	 * 注意,在转换字符流的时候,设置的字符集编码要与读取的文件的数据的编码一致
	 * 不然就会出现乱码
	 * OutputStreamWriter
	 */
public static void testOutputStreamWriter() throws Exception{
    FileOutputStream out = new FileOutputStream("D:\\testdemo\\demo\\src\\day13\\tt6.txt");

    //OutputStreamWriter os = new OutputStreamWriter(out, "UTF-8");
    OutputStreamWriter os = new OutputStreamWriter(out, "GBK");

    os.write("你好你好");
    os.flush();

    os.close();
    out.close();
}

2.3 处理流之三:标准输入输出流

​ System.in 和 System.out 分别代表了系统标准的输入和输出设备。在数据量很大时,使用标准输入流的效率比Scanner要高。

/**
	 * 标准的输入流
	 * @throws Exception
	 */
public static void testSystemIn() throws Exception{
    //创建一个接收键盘输入数据的输入流
    InputStreamReader is = new InputStreamReader(System.in);

    //把输入流放到缓冲流里
    BufferedReader br = new BufferedReader(is);

    String str = "";//定义一个临时接收数据的字符串
	//每次读取一行并打印
    while((str = br.readLine()) != null){
        System.out.println(str);
    }

    br.close();
    is.close();
}
/**
	 * 把控制台输入的内容写到指定的TXT文件中,当接收到字符串over,就结束程序的运行
	 */
public static void write2TXT() throws Exception{
    //创建一个接收键盘输入数据的输入流
    InputStreamReader is = new InputStreamReader(System.in);

    //把输入流放到缓冲流里
    BufferedReader br = new BufferedReader(is);

    BufferedWriter out = new BufferedWriter(new FileWriter("D:\\testdemo\\demo\\src\\day13\\tt7.txt"));

    String line = "";
	//接收到over则停止写入
    while((line = br.readLine()) != null){
        if(line.equals("over")){
            break;
        }
        //读取的每一行都写到指定的TXT文件
        out.write(line);
    }

    out.flush();
    out.close();
    br.close();
    is.close();
}

2.4 处理流之四:打印流

2.5 处理流之五:数据流

数据流,专门用来做基本数据类型的读写的流类。

数据输出流:

/**
	 * 数据输出流
	 * 用数据输出流写到文件的中的基本数据类型的数据,是乱码的,不能直接辨认出来,需要数据输入流来读取
	 * DataOutputStream
	 */
public static void testDataOutputStream() throws Exception{
    DataOutputStream out = new DataOutputStream(new FileOutputStream("D:/testdemo/demo/src/day13/tt8.txt"));

    //		out.writeBoolean(true);
    out.writeDouble(1.35d);
    //		out.writeInt(100);

    out.flush();
    out.close();

}

数据输入流:

/**
	 * 数据输入流
	 * 用数据输出流写到文件的中的基本数据类型的数据,是乱码的,不能直接辨认出来,需要数据输入流来读取
	 * 用数据输入流读取数据输出流写到文件中的数据时,要保证使用和当时写的数据类型一致的类型来读取
	 * 也就是说,如果写的时候是writeDouble,读的时候就得是readDouble
	 * DataInputStream
	 */
public static void testDataInputStream() throws Exception{
    DataInputStream in = new DataInputStream(new FileInputStream("D:/testdemo/demo/src/day13/tt8.txt"));

    System.out.println(in.readDouble());

    in.close();
}

2.6 处理流之六:对象流(对象的序列化与反序列化)

​ 用于存储和读取对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。(用途:对象存储在硬盘(对象的持久化),以及对象的网络传输,因为两者都基于二进制,所以,需要将对象进行序列化,读取时需要反序列化)。

序列化(Serialize):用ObjectOutputStream类将一个Java对象写入IO流中。序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。

​ 如果需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:

  • Serializable
  • Externalizable

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
private static final long serialVersionUID;
serialVersionUID用来表明类的不同版本间的兼容性
如果类没有显示定义这个静态变量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的源代码作了修改,serialVersionUID 可能发生变化。故建议,显示声明
显示定义serialVersionUID的用途

  • 希望类的不同版本对序列化兼容,因此需确保类的不同版本具有相同的serialVersionUID
  • 不希望类的不同版本对序列化兼容,因此需确保类的不同版本具有不同的serialVersionUID

使用方法: 创建一个 ObjectOutputStream,调用 ObjectOutputStream 对象的 writeObject(对象) 方法输出可序列化对象。注意写出一次,操作flush()。

反序列化(Deserialize):用ObjectInputStream类从IO流中恢复该Java对象。不能序列化static和transient修饰的对象。

使用方法:创建一个 ObjectInputStream,调用 readObject() 方法读取流中的对象。

代码示例:

先要创建可序列化的对象所属类

/**
 * 可以序列化与反序列化的对象
 * 实现Serializable接口
 */
public class Person implements Serializable{

	/**
	 * 一个表示序列化版本标识符的静态变量
	 * 用来表明类的不同版本间的兼容性
	 */
	private static final long serialVersionUID = 1L;
	
	public String name;
	public int age;

}

注意:对象的序列化与反序列化使用的类要严格一致,包名,类名,类机构等等所有都要一致

对象的序列化:

/**
	 * 对象的序列化
	 */
public static void testSerialize() throws Exception{
    //定义对象的输出流,把对象的序列化之后的流放到指定的文件中
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:/testdemo/demo/src/day13/tt9.txt"));

    //将类实例化为对象
    Person p = new Person();
    p.name = "zhangsan";
    p.age = 11;

    //写入内存
    out.writeObject(p);
    out.flush();//刷写数据到硬盘

    out.close();
}

对象的反序列化:

/**
	 * 对象的反序列化
	 */
public static void testDeserialize() throws Exception{
    //创建对象输入流对象,从指定的文件中把对象序列化后的流读取出来
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:/testdemo/demo/src/day13/tt9.txt"));

    Object obj = in.readObject();

    Person p = (Person)obj;

    System.out.println(p.name);
    System.out.println(p.age);

    in.close();

}

2.7 文件的随机读写操作

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

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。

RandomAccessFile 类对象可以自由移动记录指针:
long getFilePointer():获取文件记录指针的当前位置。
void seek(long pos):将文件记录指针定位到 pos 位置。

创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:

  • r: 以只读方式打开
  • rw:打开以便读取和写入
  • rwd:打开以便读取和写入;同步文件内容的更新
  • rws:打开以便读取和写入;同步文件内容和元数据的更新

代码示例:

/**
	 * 随机读文件
	 */
public static void testRandomAccessFileRead() throws Exception{
    //RandomAccessFile的构造有两个参数,参数1是读写的文件的路径
    //参数2是指定 RandomAccessFile 的访问模式
    //r: 以只读方式打开
    //rw:打开以便读取和写入
    //rwd:打开以便读取和写入;同步文件内容的更新
    //rws:打开以便读取和写入;同步文件内容和元数据的更新
    //最常用是r和rw

    RandomAccessFile ra = new RandomAccessFile("D:/testdemo/demo/src/day13/tt10.txt", "r");

    //ra.seek(0);//设置读取文件内容的起始点
    ra.seek(8);//通过设置读取文件内容的起始点,来达到从文件的任意位置读取

    byte[] b = new byte[1024];

    int len = 0;

    while((len = ra.read(b)) != -1){
        System.out.println(new String(b, 0, len));
    }

    ra.close();
}
/**
	 * 随机写
	 */
public static void testRandomAccessFileWrite() throws Exception{
    RandomAccessFile ra = new RandomAccessFile("D:/testdemo/demo/src/day13/tt10.txt", "rw");

    //ra.seek(0);//设置写的起始点,0代表从开头写
    //注意:如果是在文件开头或者中间的某个位置开始写的话,就会用写的内容覆盖掉等长度的原内容
    ra.seek(ra.length());//设置写的起始点,ra.length()代表从文件的最后结尾写,也就是文件的追加

    ra.write("你好".getBytes());

    ra.close();
}

第十一章 反射

​ 反射机制,就是通过一个抽象的类名能够在自己记忆(加载类的内存)中找到相匹配的类的具体信息。

​ 有没有一个统一的方式来描述这些类呢?有没有这样一个类,可以对随意的类进行高度的抽象,形成一个可以描述所有类的类?有,就是Class。

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

Java反射机制提供的功能

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

Java反射机制研究及应用:

反射相关的主要API:

java.lang.Class:代表一个类

java.lang.reflect.Method:代表类的方法

java.lang.reflect.Field:代表类的成员变量

java.lang.reflect.Constructor:代表类的构造方法

1. Class类

​ 在Object类中定义了以下的方法,此方法将被所有子类继承:
public final Class getClass();

​ 以上的方法返回值的类型是一个Class类,此类是Java反射的源头, 实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称.

​ 反射可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个类的有关信息。
注意:

  1. Class本身也是一个类,Class 对象只能由系统建立对象
  2. 一个类在 JVM 中只会有一个Class实例
  3. 一个Class对象对应的是一个加载到JVM中的一个.class文件
  4. 每个类的实例都会记得自己是由哪个 Class 实例所生成
  5. 通过Class可以完整地得到一个类中的完整结构

Class类的常用方法

方法名
static Class forName(String name)根据类的全类名(包名+类名)获取Class对象
Object newInstance()创建目标类对象
getName()获取全类名
Class getSuperclass()获取所有的父类的Class对象
Class [] getInterfaces()获取所有实现的接口
ClassLoader getClassLoader()获取类 的类加载器
Class getSuperclass()获取父类的Class对象
Constructor[] getConstructors()获取所有的构造器
Field[] getDeclaredFields()获取所有的属性
Method getMethod(String name,Class paramTypes)获取对应的方法

代码示例:

//创建Person类
public class Person {
	public String name;
	int age;
}
//实例化Class类对象的方法
public class Test {
	public static void main(String[] args) {
		Person p = new Person();
		Class clazz = p.getClass();//clazz对象中就包含对象p所属的Person类的所有的信息
		
		//方法1:若已知具体的类,通过类的.class属性获取,该方法最为安全可靠,程序性能最高
		Class c0 = Person.class;//通过类名.class创建指定类的Class实例
        
		//方法2:已知某个类的实例,调用该实例的getClass()方法获取Class对象
		Class c1 = p.getClass();//通过一个类的实例对象的getClass()方法,获取对应实例对象的类的Class实例
		
        //方法3:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
		try {
			//通过Class的静态方法forName(String className)来获取一个类的Class实例
			//forName(String className)方法中的参数是你要获取的Class实例的类的全路径(包名.类名)
			Class c2 = Class.forName("day14.Person");//这个是获取Class实例的常用方式
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
        
        //方法4:
        ClassLoader cl = this.getClass().getClassLoader();
      	Class c3 = cl.loadClass("类的全类名");

		
	}
}

2. 通过反射调用类的完整结构

使用反射可以取得:

格式说明: public Class<?>[] 为 返回值类型, getInterface()为调用方法e. g. clazz.getInterface().

  1. 实现的全部接口
    public Class<?>[] getInterfaces()
    确定此对象所表示的类或接口实现的接口。

  2. 所继承的父类
    public Class<? Super T> getSuperclass()
    返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。

  3. 全部的构造器

    使用方法见下面第三节

    ​ public Constructor[] getConstructors()
    返回此 Class 对象所表示的类的所有public构造方法,返回的是一个Constructor类数组,其中还有以下方法。
    ​ public Constructor[] getDeclaredConstructors()
    返回此 Class 对象表示的类声明的所有构造方法(包含private的构造方法)。
    Constructor类中

    取得修饰符: public int getModifiers(); 返回1为public,2为private

    取得方法名称: public String getName();

    取得参数的类型:public Class<?>[] getParameterTypes();

  4. 全部的方法

    使用方法见下面第四节

    ​ public Method[] getDeclaredMethods()
    返回此Class对象所表示的类或接口的全部方法
    ​ public Method[] getMethods()
    返回此Class对象所表示的类或接口的public的方法
    Method类中

    public Class<?> getReturnType()取得全部的返回值

    public Class<?>[] getParameterTypes()取得全部的参数

    public int getModifiers()取得修饰符

  5. 全部的Field

    使用方法见下面第五节

    ​ public Field[] getFields()
    返回此Class对象所表示的类或接口的public的Field。
    ​ public Field[] getDeclaredFields()
    返回此Class对象所表示的类或接口的全部Field
    Field方法中

    public int getModifiers() 以整数形式返回此Field的修饰符

    public Class<?> getType() 得到Field的属性类型

    public String getName() 返回Field的名称。

  6. 类所在的包
    使用方法见下面第五节
    Package getPackage()

3. 通过反射创建一个对象

//如果用反射的构造方法来创建对象
try {
    Class clazz = Class.forName("day14.Student");//通过包名.类名的字符串,调用Class.forName方法获取指定类的Class实例
    Object obj = clazz.newInstance();//相当于调用Student类的无参公有的构造方法
    Student stu = (Student)obj;

    Constructor c = clazz.getConstructor(String.class);//指定获取有一个参数并且为String类型的公有的构造方法
    Student stu1 = (Student)c.newInstance("第一中学");//newInstance实例化对象,相当于调用public Student(String school)
    System.out.println(stu1.school);

    //通过反射机制,可以强制的调用私有的构造方法
    Constructor c = clazz.getDeclaredConstructor(String.class,int.class);//指定获取有两个参数(String,int)的构造方法

    c.setAccessible(true);//解除私有的封装,下面就可以对这个私有方法强制调用				
    Student stu = (Student)c.newInstance("zhangsan",12);

} catch (Exception e) {
    e.printStackTrace();
}

4. 通过反射获取类的方法


try {
    	Class clazz = Class.forName("day14.Student");//通过包名.类名的字符串,调用Class.forName方法获取指定类的Class实例
		
//    Method[] ms = clazz.getMethods();//获取到类的所有公有的方法
    Method[] ms = clazz.getDeclaredMethods();//获取类所有方法,包含公有和私有
    for(Method m : ms){
        System.out.println("方法名:" + m.getName());
        System.out.println("返回值类型:" + m.getReturnType());
        System.out.println("修饰符:" + m.getModifiers());

        Class[] pcs = m.getParameterTypes();//获取方法的参数类型,是一个数组,方法有几个参数,数据就有几个元素
        if(pcs != null && pcs.length > 0){
            for(Class pc : pcs){
                System.out.println("参数类型:" + pc.getName());
            }
        }

        System.out.println("==============================================");

    }
} catch (Exception e) {
    e.printStackTrace();
}

5. 通过反射获取类的属性和所属包


try {
    	Class clazz = Class.forName("day14.Student");//通过包名.类名的字符串,调用Class.forName方法获取指定类的Class实例
//		Field[] fs = clazz.getFields();//获取类的公有的属性,包含父类的公有属性
			
		Field[] fs = clazz.getDeclaredFields();//获取本类的(不包括父类的属性)所有的属性,包括私有
			
		for(Field f : fs){
			System.out.println("修饰符:" + f.getModifiers());
			System.out.println("属性的类型:" + f.getType());
			System.out.println("属性的名称:" + f.getName());
		}

		Package p = clazz.getPackage();//获取类所在的包
		System.out.println(p.getName());

} catch (Exception e) {
    e.printStackTrace();
}

6. 通过反射调用指定方法

try {
    Class clazz = Class.forName("day14.Student");//通过包名.类名的字符串,调用Class.forName方法获取指定类的Class实例
    /**
	* 注意:下面不论是反射调用setInfo还是test方法
	* 都调用的obj对象的方法,obj对象时间上就是student对象
	*/
    //要调用方法,先要有对象
    Constructor con = clazz.getConstructor();//获取无参构造
    Object obj = con.newInstance();//使用无参构造创建对象

    Method m = clazz.getMethod("setInfo", String.class,String.class);//得到名称叫setInfo,参数是String,String的方法
    m.invoke(obj, "zhangsan","第一中学");//参数1是需要实例化的对象,后面的参数是调用当前的方法实际参数

    //如果想要调用一个私有方法
    Method m1 = clazz.getDeclaredMethod("test", String.class);//获取方法名为test,参数为1个String类型的方法

    m1.setAccessible(true);//解除私有的封装,下面可以强制调用私有的方法

    m1.invoke(obj, "李四");

    //调用一个重载方法
    Method m2 = clazz.getMethod("setInfo", int.class);//setInfo的重载方法
    m2.invoke(obj, 1);

    //有返回值的方法
    Method m3 = clazz.getMethod("getSchool");//这是获取方法名为getSchool并且没有参数的方法
    String school = (String)m3.invoke(obj);//调用有返回值的但是没有参数的方法
    System.out.println(school);

} catch (Exception e) {
    e.printStackTrace();
}

7. 反射调用指定属性

try {
    Class clazz = Class.forName("day14.Student");//通过包名.类名的字符串,调用Class.forName方法获取指定类的Class实例
    //反射创建一个对象
    Constructor con = clazz.getConstructor();
    Student stu = (Student)con.newInstance();

    Field f = clazz.getField("school");//获取名称为school的属性

    f.set(stu, "第三中学");//对stu对象的school属性设置值"第三中学"
    String school = (String)f.get(stu);//获取stu对象的school属性的值
    System.out.println(school);

    //如果是私有的属性
    Field f1 = clazz.getDeclaredField("privateField");

    f1.setAccessible(true);//解除私有的封装,下面就可以强制的调用这个属性

    f1.set(stu, "测试私有属性");
    System.out.println(f1.get(stu));

} catch (Exception e) {
    e.printStackTrace();
}

8. Java的动态代理

​ 假如一个java项目,其中有100 java类,每个java类有10个方法,这总共1000个方法现在有这样一个需求,需要在每个java方法上加上2行相同的代码,在方法执行前输出这个方法开始执行,在方法执行后输出这个方法已经完成,手动完成显然不可能,Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。

代码示例

//接口
public interface ITestDemo {
	void test1();
	void test2();
}
//实现接口的类
public class TestDemoImpl implements ITestDemo {

	@Override
	public void test1() {
		System.out.println("执行test1()方法");
	}

	@Override
	public void test2() {
		System.out.println("执行test2()方法");
	}

}
//动态代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 动态代理类
 *
 */
public class ProxyDemo implements InvocationHandler{

	Object obj;//被代理的对象
	
	public ProxyDemo(Object obj){
		this.obj = obj;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		
		System.out.println(method.getName() + " 方法开始执行");
		
		Object result = method.invoke(this.obj, args);//执行的是指定代理对象的指定的方法
		
		System.out.println(method.getName() + " 方法执行完毕");
		return result;
	}

}
//主类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test {
	public static void main(String[] args) {
		ITestDemo test = new TestDemoImpl();
		/**
		 * 注意:如果一个对象想要通过Proxy.newProxyInstance方法被代理,
		 * 那么这个对象的类一定要有相应的接口
		 * 就像本类中的ITestDemo接口和实现类TestDemoImpl
		 */
		test.test1();
		test.test2();
		System.out.println("======================");
		/**
		 * 需求:
		 * 在执行test1和test2方法时需要加入一些东西
		 * 在执行方法前打印test1或test2开始执行
		 * 在执行方法后打印test1或test2执行完毕
		 * 打印的方法名要和当时调用方法保存一致
		 */
		
		InvocationHandler handler = new ProxyDemo(test);
		/**
		 * Proxy.newProxyInstance(ClassLoader, interfaces, h)
		 * 参数1是代理对象的类加载器
		 * 参数2是被代理的对象的接口
		 * 参数3是代理对象
		 * 
		 * 返回的值就成功被代理后对象,返回的是Object类型,需要根据当时的情况去转换类型
		 */
		ITestDemo t = (ITestDemo)Proxy.newProxyInstance(handler.getClass().getClassLoader(), test.getClass().getInterfaces(), handler);
		
		t.test1();
		System.out.println("-----------------------");
		t.test2();
		
	}
}
  • 注意:如果一个对象想要通过Proxy.newProxyInstance方法被代理,
  • 那么这个对象的类一定要有相应的接口
  • 就像本类中的ITestDemo接口和实现类TestDemoImpl

动态代理步骤

1.创建一个实现接口InvocationHandler的类,它必须实现invoke方法,以完成代理的具体操作。

2.创建被代理的类以及接口

3.通过Proxy的静态方法

newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject接口代理

4.通过 Subject代理调用RealSubject实现类的方法


第十二章 线程

1. 基本概念:程序 - 进程 - 线程

程序(program) :是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process): 是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。

线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。

CPU 核心数: 有几个核心就代表同一时刻能够处理多少个任务(进程),所以现实中的电脑在不同的进程之间不停快速切换,人类无法察觉。

主频 就是CPU在不同进程之间切换的频率。

可以把进程理解为一条河流,而开启了线程就相当于河流有了支流。

2. 多线程的创建和启动

何时需要多线程:

  • 程序需要同时执行两个或多个任务。

  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。

  • 需要一些后台运行的程序时。

Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现。

Thread类的特性:

  1. 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,想要在开启的多线程中运行的代码逻辑就写在run()方法中。
  2. 通过该Thread对象的start()方法来调用启动这个线程,本质上就是开始运行run()方法

2.1 创建线程的两种方式

2.1.1. 继承Thread类
  1. 定义子类继承Thread类。

  2. 子类中重写Thread类中的run方法。

  3. 创建Thread子类对象,即创建了线程对象。

  4. 调用线程对象start方法:启动线程,调用run方法。

代码示例:

/**
 * 定义子类继承Thread类
 * 继承Thread的方式实现多线程
 */
public class TestThread extends Thread{
	@Override
    //重写run()方法
	public void run() {
		System.out.println("多线程运行的代码");
		for(int i = 0; i < 5; i++){
			System.out.println("这是多线程的逻辑代码:" + i);
		}
	}
}
public class Test {
	public static void main(String[] args) {
        
		Thread t0 = new TestThread();//类的多态,用父类接收子类,继承Thread类的线程
		t0.start();//启动线程,开始运行run方法中的代码,后续还可以根据需要开启更多线程
//		new Test().test();
		System.out.println("---------------1");
		System.out.println("---------------2");
		System.out.println("---------------3");
		
		/**
		 * 多次运行这个main方法之后
		 * 我们发现main方法中打印的3行与开启线程运行run方法中的打印语句是混合起来
		 * 而且main方法中的打印与run方法中打印语句顺序是不固定的
		 * 为什么呢?
		 * main执行t0.start()方法开启多线程之后,就相当于在main方法之外开启一个支流
		 * 这个个时候t0.start()的之后的main方法的其他代码的运行就与run方法运行无关了
		 * 以当前代码为例
		 * t0.start()的之后的main方法的其他代码与run方法的代码并行运行
		 * 就像两条河流一样,各走各的
		 * 那么控制台输出的结果就是两条并行程序的运行结果总和,这个结果需要拆开成两部分看
		 * 就可以看到,各自是保持自己输出顺序
		 * 这个就是多线程的异步,这个异步相对于执行t0.start()的主程序来说的
		 * 简单来说开启了线程之后run方法中运行的代码主程序中t0.start()之后的程序是并行执行的,没先后关系,这个叫异步
		 */
	}
	
	public void test(){
		System.out.println("main线程运行的代码");
		for(int i = 0; i < 5; i++){
			System.out.println("这是main逻辑代码:" + i);
		}
	}
}
2.1.2 实现Runnable接口

1)定义子类,实现Runnable接口。

2)子类中重写Runnable接口中的run方法。

3)通过Thread类含参构造器创建线程对象。

4)将Runnable接口的子类对象作为实际参数传递给

Thread类的构造方法中。

5)调用Thread类的start方法:开启线程,调用

Runnable子类接口的run方法。

代码示例:

/**
 * 通过实现Runnable接口方式实现多线程
 */
public class TestRunnable implements Runnable{

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ":Runnable多线程运行的代码"); //Thread.currentThread().getName()为获取线程名称的方法,配合t4中传入参数使用
		for(int i = 0; i < 5; i++){
			count++;
			System.out.println(Thread.currentThread().getName() + ":这是Runnable多线程的逻辑代码" );
		}
	}

}
public class Test {
	public static void main(String[] args) {

		Thread t3 = new Thread(new TestRunnable());
		t3.start();
		
		Thread t4 = new Thread(new TestRunnable(), "t-1");//加入线程名称参数,在TestRunnable类中
		t4.start();
		Thread t5 = new Thread(new TestRunnable(), "t-2");
		t5.start();
//		new Test().test();
		System.out.println("---------------1");
		System.out.println("---------------2");
		System.out.println("---------------3");

	}
	
	public void test(){
		System.out.println("多线程运行的代码");
		for(int i = 0; i < 5; i++){
			System.out.println("这是多线程的逻辑代码:" + i);
		}
	}
}

2.2.继承方式和实现方式的联系与区别

区别:

继承Thread: 线程代码存放Thread子类run方法中。重写run方法

**实现Runnable:**线程代码存在接口的子类的run方法。实现run方法

实现Runnable接口方式开启多线程的好处

1)避免了单继承的局限性,如果使用继承Thread方式,因为Java类不能多重继承,所以继承了Thread就不能再继承其他类了。

2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

所以一般使用实现接口方式来实现多线程

示例代码:

/**
 * 通过实现Runnable接口方式实现多线程
 */
public class TestRunnable implements Runnable{

	int count = 0;
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ":Runnable多线程运行的代码"); //Thread.currentThread().getName()为获取线程名称的方法,配合t4中传入参数使用
		for(int i = 0; i < 5; i++){
			count++;
			System.out.println(Thread.currentThread().getName() + ":这是Runnable多线程的逻辑代码:" + count);
		}
	}
}

public class Test {
	public static void main(String[] args) {
        //将new创建的TestRunnable对象接收,然后向t4,t5传入同一个run,共享资源
        //运行后count会自加10次,从而实现多线程操作同一份资源。
		Runnable run = new TestRunnable();
        
		Thread t4 = new Thread(run, "t-1");
		t4.start();
		Thread t5 = new Thread(run, "t-2");
		t5.start();
//		new Test().test();
		System.out.println("---------------1");
		System.out.println("---------------2");
		System.out.println("---------------3");
	}
	
	public void test(){
		System.out.println("多线程运行的代码");
		for(int i = 0; i < 5; i++){
			System.out.println("这是多线程的逻辑代码:" + i);
		}
	}
}

2.3 使用多线程的优点

  • 多线程程序的优点:
  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

  2. 提高计算机系统CPU的利用率

  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

如果在一个很长的方法中,各段代码互不相干,就可以使用多线程处理。

2.4 Thread类的有关方法

void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称,如果在创建线程的时候没有指定名称,系统会给出默认名称,通过getName()获取线程名称
void setName(String name): 设置该线程名称
static currentThread(): 返回当前线程

/**

  • 线程的优先级,就是哪个线程有较大个概率被执行
  • 优先级是用数组1-10表示,数字越大优先级越高,如果没有设置默认优先级是5
    */

getPriority() : 返回线程优先值
setPriority(int newPriority) : 改变线程的优先级

static void yield(): 线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法

join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完才继续执行后续代码。低优先级的线程也可以获得执行。

static void sleep(long millis)(指定时间:毫秒)令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。抛出InterruptedException异常

stop(): 强制线程生命期结束

boolean **isAlive():**返回boolean,判断线程是否还活着

示例代码:

public class Test1 {
	public static void main(String[] args) {
		TestRun run0 = new TestRun();
		TestRun run1 = new TestRun();
		
		Thread t0 = new Thread(run0);
		
		Thread t1 = new Thread(run1);
		
		t0.setName("线程t-0");//设置线程的名称
		t1.setName("线程t-1");//设置线程的名称
		
//		t0.setPriority(1);//设置线程的优先级
//		t1.setPriority(10);//设置线程的优先级
		
		t0.start();
		t1.start();
//		System.out.println(t0.getName());//如果在创建线程的时候没有指定名称,系统会给出默认名称,通过getName()获取线程名称
//		System.out.println(t1.getName());
		
		/**
		 * 线程的优先级,就是哪个线程有较大个概率被执行
		 * 优先级是用数组1-10表示,数字越大优先级越高,如果没有设置默认优先级是5
		 */
		
//		System.out.println("t0的优先级:" + t0.getPriority());//获取线程的优先级
		
		System.out.println("---------------1");
		System.out.println("---------------2");
		
		System.out.println(t1.isAlive());//判断当前的线程是否存活
		
		t1.stop();//强制线程生命期结束,强制停止此线程
		
		try {
			t0.join();//相当于在这块把t0的run的代码插入到这个位置执行
			/**
			 * 专业的说法
			 * 就是阻塞当前的main方法,先不执行System.out.println("---------------3");代码
			 * 先执行join进来的线程的代码
			 * join的线程执行完毕之后继续执行之前main方法阻塞的代码System.out.println("---------------3");
			 */
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		System.out.println("---------------3");
		
		System.out.println(t1.isAlive());
	}
}

class TestRun implements Runnable{
	int count = 0;
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ":Runnable多线程运行的代码");
		for(int i = 0; i < 5; i++){
//			try {
//				Thread.sleep(1000);//当前线程睡眠1000毫秒
//				//相当于当前的这个循环每隔1000毫秒执行一次循环
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
			
//			if(i % 2 == 0){
//				Thread.yield();//线程让步
//			}
			
			count++;
			System.out.println(Thread.currentThread().getName() + ":这是Runnable多线程的逻辑代码:" + count);
		}
	}
	
}

3. 线程的生命周期

JDK中用Thread.State枚举表示了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

Ø新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

Ø**就绪:**处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件

Ø**运行:**当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能

Ø**阻塞:**在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态

Ø**死亡:**线程完成了它的全部工作或线程被提前强制性地中止

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mbg2CPpe-1587194076072)(Java Record.assets/image-20200418140401840.png)]

4. 线程的同步与锁死

​ 多个线程执行的不确定性引起执行结果的不稳定

​ 多个线程对账本的共享,会造成操作的不完整性,会破坏数据,即使加入了判断语句,但是多个线程核能同时满足条件,从而越过条件(当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。),解决方法:直接在方法上加上同步锁,限制对多条操作共享数据的语句,只能同时让一个线程完整执行完,在执行过程中,其他线程不可以参与执行。

Synchronized的使用方法

Java对于多线程的安全问题提供了专业的解决方式:
同步机制

  1. synchronized还可以放在方法声明中,表示整个方法
    为同步方法。
    例如:
    public synchronized void show (String name){
    ….
    }

  2. synchronized (对象){
    // 需要被同步的代码;
    }

    如果针对对象要加同步锁,那就加在方法上,如果针对某段代码要加同步锁,那就直接在代码块上加同步锁

public class Test2 {
	public static void main(String[] args) {
		//定义账户对象
		Acount a = new Acount();
		Acount a1 = new Acount();
		
		//多线程对象
		User u_weixin = new User(a, 2000);
		User u_zhifubao = new User(a, 2000);
		
		Thread weixin = new Thread(u_weixin,"微信");
		Thread zhifubao = new Thread(u_zhifubao,"支付宝");
		
		weixin.start();
		zhifubao.start();
	}
}

class Acount{
	public static int money = 3000;//全局变量,所有的操作共享这个变量
	
	/**
	 * 提款,判断账户钱够不够
	 * 多线程调用这个方法,就有问题,线程共享资源时,一个线程在执行这个方法没有完毕时,另一个线程又开始执行这个方法
	 * 解决思路:显然一个线程整体执行完这个方法,另一个线程再执行
	 * 通过synchronized同步锁来完成
	 * 可以直接在方法上加上synchronized关键字
	 * 在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法
	 * 不同的对象就是不同的锁,普通方法加synchronized,线程使用不同的此方法的对象,还有共享资源的问题
	 *  静态的方法加synchronized,对于所有的对象都是使用同一个一个锁
	 * 同一个对象的不同方法加同步锁,锁的当前方法对应的对象,当前的对象的所有加了同步锁的方法是共用一个同步锁
	 * @param m
	 */
	public synchronized void drawing(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,账户金额不足:" + money);
		}else{
			System.out.println(name + "操作,账户原有金额:" + money);
			System.out.println(name + "操作,取款金额:" + m);
			
			System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
			money = money - m;
			System.out.println(name + "操作,取款后的余额:" + money);
		}
		
	}
}
class User implements Runnable{
	Acount acount;
	int money;
	public User(Acount acount,int money){
		this.acount = acount;
		this.money = money;
	}
	@Override
	public void run() {
		acount.drawing(money);
	}
	
}

多种加同步锁方式例程

public class Test2 {
	public static void main(String[] args) {
		//定义账户对象
		Acount a = new Acount();
		Acount a1 = new Acount();
		
		//多线程对象
		User u_weixin = new User(a, 2000);
		User u_zhifubao = new User(a, 2000);
		
		Thread weixin = new Thread(u_weixin,"微信");
		Thread zhifubao = new Thread(u_zhifubao,"支付宝");
		
		weixin.start();
		zhifubao.start();
	}
}

class Acount{
	public static int money = 3000;//全局变量,所有的操作共享这个变量
	
	/**
	 * 提款,判断账户钱够不够
	 * 多线程调用这个方法,就有问题,线程共享资源时,一个线程在执行这个方法没有完毕时,另一个线程又开始执行这个方法
	 * 解决思路:显然一个线程整体执行完这个方法,另一个线程再执行
	 * 通过synchronized同步锁来完成
	 * 可以直接在方法上加上synchronized关键字
	 * 在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法
	 * 不同的对象就是不同的锁,普通方法加synchronized,线程使用不同的此方法的对象,还有共享资源的问题
	 * 
	 * 普通方法加同步锁,锁的当前方法对应的对象,当前的对象的所有加了同步锁的方法是共用一个同步锁
	 * @param m
	 */
	public synchronized void drawing(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,账户金额不足:" + money);
		}else{
			System.out.println(name + "操作,账户原有金额:" + money);
			System.out.println(name + "操作,取款金额:" + m);
			
			System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
			money = money - m;
			System.out.println(name + "操作,取款后的余额:" + money);
		}
		
	}
	
	public synchronized void drawing1(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,账户金额不足:" + money);
		}else{
			System.out.println(name + "操作,账户原有金额:" + money);
			System.out.println(name + "操作,取款金额:" + m);
			
			System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
			money = money - m;
			System.out.println(name + "操作,取款后的余额:" + money);
		}
		
	}
	
	/**
	 * 静态的方法加synchronized,对于所有的对象都是使用同一个一个锁
	 * @param m
	 */
	public static synchronized void drawing2(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,账户金额不足:" + money);
		}else{
			System.out.println(name + "操作,账户原有金额:" + money);
			System.out.println(name + "操作,取款金额:" + m);
			
			System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
			money = money - m;
			System.out.println(name + "操作,取款后的余额:" + money);
		}
		
	}
	
	/**
	 * 对代码块加入同步锁
	 * 代码块synchronized(this),所有当前的对象的synchronized(this)同步的的代码都是使用同一个锁
	 * @param m
	 */
	public void drawing3(int m){
		synchronized(this){//表示当前的对象的代码块被加了synchronized同步锁
			//用this锁代码块是代表当前的对象,如果在其他方法中也有synchronized(this)的代码块使用的都是同一个同步锁
			String name = Thread.currentThread().getName();
			
			if(money < m){
				System.out.println(name + "操作,账户金额不足:" + money);
			}else{
				System.out.println(name + "操作,账户原有金额:" + money);
				System.out.println(name + "操作,取款金额:" + m);
				
				System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
				money = money - m;
				System.out.println(name + "操作,取款后的余额:" + money);
			}
		}
	}
	
	public void drawing4(int m){
		synchronized(this){//表示当前的对象的代码块被加了synchronized同步锁
			//用this锁代码块是代表当前的对象,如果在其他方法中也有synchronized(this)的代码块使用的都是同一个同步锁
			String name = Thread.currentThread().getName();
			
			if(money < m){
				System.out.println(name + "操作,账户金额不足:" + money);
			}else{
				System.out.println(name + "操作,账户原有金额:" + money);
				System.out.println(name + "操作,取款金额:" + m);
				
				System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
				money = money - m;
				System.out.println(name + "操作,取款后的余额:" + money);
			}
		}
	}
	
	/**
	 * synchronized修饰代码块,想要根据不同的对象有不同的锁
	 * synchronized(a),这个小括号中传入不同的对象就是不同的锁
	 * @param m
	 */
	public void drawing5(int m,Acount a){
		synchronized(a){//表示通过方法的参数传递进来的对象的代码块被加了synchronized同步锁
			//不同的对象就有不同的同步锁
			String name = Thread.currentThread().getName();
			
			//线程通信:如果是微信操作的,先不执行,等支付宝操作,支付宝操作完,微信再继续操作
			if(name.equals("微信")){
				try {
					a.wait();//当前的线程进入等待的阻塞状态
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			
			if(money < m){
				System.out.println(name + "操作,账户金额不足:" + money);
			}else{
				System.out.println(name + "操作,账户原有金额:" + money);
				System.out.println(name + "操作,取款金额:" + m);
				
				System.out.println(name + "操作,取款操作:原金额" + money + " - " + "取款金额" + m);
				money = money - m;
				System.out.println(name + "操作,取款后的余额:" + money);
			}
			
			if(name.equals("支付宝")){
				try {
					a.notify();//唤醒当前优先级最高的线程,进入就绪状态
//					a.notifyAll();//唤醒当前所有的线程,进入就绪状态
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
} 

class User implements Runnable{
	Acount acount;
	int money;
	public User(Acount acount,int money){
		this.acount = acount;
		this.money = money;
	}
	@Override
	public void run() {
//		acount.drawing(money);
//		if(Thread.currentThread().getName().equals("微信")){
			acount.drawing(money);
//			acount.drawing3(money);
//		}else{
			acount.drawing1(money);
//			acount.drawing4(money);
//		}
//		acount.drawing2(money);//调用类的静态方法
		
//		acount.drawing3(money);
		
		acount.drawing5(money, acount);
	}
	
}

5. 线程死锁问题介绍

死锁

​ 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

解决方法:

Ø专门的算法、原则,比如加锁顺序一致

Ø尽量减少同步资源的定义,尽量避免锁未释放的场景

6. 线程通信

wait() notify() notifyAll(),这三个方法只能用于有同步锁的方法或代码块

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

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

ØnotifyAll ():唤醒正在排队等待资源的所有线程结束等待.

Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常

wait() 方法:在当前线程中调用方法: 对象名.wait()

使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。

调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

调用此方法后,当前线程将释放对象监控权 ,然后进入等待

在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll():

在当前线程中调用方法: 对象名.notify()

功能:唤醒等待该对象监控权的一个线程。

调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

7. 例程:生产者与消费者问题

​ 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能出现两个问题:

  1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  2. 消费者比生产者快时,消费者会取相同的数据。

代码示例:

public class Test {
	public static void main(String[] args) {
		Clerk c = new Clerk();
		//消费时不生产,生产时不消费
		
		//生产者
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (c) {
					while(true){//无限循环代表无限的生产次数
						if(c.productNum == 0){//产品数为0,开始生产
							System.out.println("产品数为0,开始生产");
							while(c.productNum < 4){
								c.productNum++;//增加产品
								System.out.println("库存:" + c.productNum);
							}
							System.out.println("产品数为" + c.productNum + ",结束生产");
							
							c.notify();//唤醒消费者线程
						}else{
							try {
								c.wait();//生产者线程等待
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		}, "生产者").start();
		
		//消费者
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (c) {
					while(true){//无限循环代表无限的消费次数
						if(c.productNum == 4){//产品数为4,开始消费
							System.out.println("产品数为4,开始消费");
							while(c.productNum > 0){
								c.productNum--;//消费产品
								System.out.println("库存:" + c.productNum);
							}
							System.out.println("产品数为" + c.productNum + ",结束消费");
							
							c.notify();//唤醒生产者线程
						}else{
							try {
								c.wait();//消费者线程等待
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		}, "消费者").start();
		
	}
}


class Clerk{
	public static int productNum = 0;
}

​ 2020.4.18
版权说明:该文档由作者根据Java教程进行整理,其中涉及图片代码仅供学习,侵删。

  • 42
    点赞
  • 209
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值