Java基础全程学习笔记(二)

第06章:面向对象-基础#

面向对象内容的三条主线:
> 类及类的内部成员:属性、方法、构造器;代码块、内部类
> 面向对象的三大特征:封装性、继承性、多态性
> 其它关键字的使用:package、import、this、super、static、final、abstract、interface等

1. 理解:面向过程vs面向对象#

简单的语言描述二者的区别
> 面向过程:以`函数`为组织单位。是一种“`执行者思维`”,适合解决简单问题。扩展能力差、后期维护难度较大。

> 面向对象:以`类`为组织单位。是一种“`设计者思维`”,适合解决复杂问题。代码扩展性强、可维护性高。

2.2 二者关系:在面向对象的编程中,具体的方法中,仍然会体现面向过程的思想。所以二者是合作关系。

2. 面向对象的要素:类、对象#

  • 区分类与对象

    • 类:抽象的、概念上的定义
    • 对象:具体的,实实在在存在的,由类派生出来的
  • 设计类,就是设计类的成员:属性、方法

  • 面向对象完成具体功能的操作的三步流程(非常重要)

    步骤1:创建类,即设计类的内部成员(属性、方法)
    步骤2:创建类的对象。
    步骤3:通过"对象.属性" 或 "对象.方法"的方式,完成相关的功能。
    
  • 对象的内存解析

    • JVM内存分配:虚拟机栈、堆、方法区(目前用不到)、程序计数器(略)、本地方法栈(略)
      • 虚拟机栈:存放的是方法对应的栈帧,每个栈帧中存放方法中声明的局部变量。
      • 堆:new出来的"东西":数组实体、对象实体(含成员变量)
    • 创建类的1个对象、创建类的多个对象(内存解析图建议大家都自己画画)

3. 类的成员之一:属性(重点)#

1.变量的分类:
- 角度一:按照数据类型来分:基本数据类型(8种)、引用数据类型(数组、类、接口;注解、枚举、记录)
- 角度二:按照变量在类中声明的位置来分:成员变量、局部变量

2. 成员变量的几个称谓:

成员变量 <=> 属性 <=> field(字段、域)

3. 区分成员变量   vs 局部变量
3.1 相同点:(了解)
> 都有三个要素(数据类型、变量名、变量值)
> 声明的格式相同:数据类型 变量名 = 变量值
> 变量都是先声明后使用
> 变量都有作用域,在其作用域内是有效的

3.2 不同点:
① 类中声明的位置的不同:
> 成员变量:声明在类内部、方法等结构的外部。
> 局部变量:声明在方法内部、方法的形参、构造器的内部、构造器的形参、代码块的内部等

② 在内存中分配的位置不同:
> 成员变量:随着对象实体在堆空间进行分配而分配(或存储)
> 局部变量:存储在栈空间。

③ 生命周期:
> 成员变量:随着对象的创建而产生,随着对象的消亡而消亡
> 局部变量:(以方法为例)随着方法的调用而产生,随着方法的调用结束而消亡。
      > 拓展:每一个方法的执行,都对应着一个栈帧加载进栈中。局部变量就存储在每个方法对应的栈帧中。
             当方法执行结束时,对应的栈帧就弹出栈,进而栈帧中的局部变量也弹出,进而消亡。

④ 作用域:
> 成员变量:在整个类的内部是有效的。---> 类的方法中是可以调用类中的成员变量的。
> 局部变量:以方法为例,作用域仅限于方法内部。

⑤ 是否可以有权限修饰符进行修饰:(超纲)
> 成员变量:可以被不同的权限修饰符进行修饰。(后面讲封装性时,具体说:private、public、protected、缺省)
> 局部变量:不可以被权限修饰符进行修饰。一旦修饰,编译不通过。


⑥ 是否有默认值:
> 成员变量:都有默认值
      默认值的情况与不同类型的一维数组的元素的默认值相同。
      > 整型:0
      > 浮点型:0.0
      > 字符型:0
      > 布尔型:false
      > 引用类型:null

> 局部变量:没有默认值。
   意味着在调用之前必须要显示赋值。如果不赋值,就报错。
       > 特别的:方法的形参在方法调用时赋值即可。

4. 类的成员之二:方法(重点)#

4.1 方法的使用#
1. 使用方法的好处
将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码。

2. 使用举例
- Math.random()的random()方法
- Math.sqrt(x)的sqrt(x)方法
- System.out.println(x)的println(x)方法
- new Scanner(System.in).nextInt()的nextInt()方法
- Arrays类中的binarySearch()方法、sort()方法、equals()方法

3. 方法声明的格式

举例:public void eat()
     public void sleep(int hour)
     public String getName()
     public String playGame(String game)

格式:
     权限修饰符  返回值类型 方法名(形参列表){
        方法体
     }

4. 具体的方法声明的细节

4.1 权限修饰符:体现此方法被调用时,是否能被调用的问题。(主要放到封装性的时候讲解)
    暂时,大家在声明方法时,先都使用public修饰即可。

4.2 返回值类型:(难点)
    > 分类:有具体的返回值的类型(指明具体的数据类型) 、 没有返回值类型(使用void)
    > 情况1:有具体的返回值的类型的要求:既然有返回值的类型,则要求此方法在执行完时,一定要返回
            满足此类型的一个变量或常量。
            > 内部使用"return 变量(或常量)"的方法,返回数据
    > 情况2:没有返回值类型:内部就不需要使用return结构了。
            > (难点)其实,我们在此方法中也可以使用return,仅表示结束此方法。

    开发中,设计一个方法时,是否需要设计返回值类型?
       > 根据题目的要求设计。
       > 具体问题具体分析:调用完此方法之后,是否需要一个结果性的数据,供之后使用。如果有必要,就设计有返回值类型的场景即可。

4.3 方法名:属性标识符,定义时需要满足标识符的命名规则、规范、"见名知意"。

4.4 形参列表:(难点)
       > 在一个方法的一对小括号中可以声明形参列表,形参的个数可以为0个、1个或多个。
       > 如果有形参的话,格式为: (数据类型1 形参名1,数据类型2 形参名2,...)

      开发中,设计一个方法时,是否需要提供形参呢?
         > 根据题目的要求设计。
         > 具体问题具体分析:调用此方法时,是否存在不确定性的数据。如果有,则以形参的方法传入此不确定的数据。

4.5 方法体:即为调用方法时,执行的代码。可以使用当前方法声明的形参,使用类中的成员变量。

5. 注意点
> Java里的方法`不能独立存在`,所有的方法必须定义在类里。
> 方法内可以使用类中的成员变量
> 方法内不可以定义方法,但是方法内可以调用本类中的其它方法。 ---> 递归方法中谈方法内自己调用自己。
> 类中不可以定义多个相同的方法。---> 方法的重载

4.2 return关键字#
1. return的作用
> 作用1:结束当前方法的执行
> 作用2:"return + 变量/常量"结构在方法结束的同时,还可以返回一个数据。

2. 使用注意点:
与break、continue类似,其后不能声明执行语句。

5. 内存的分配使用#

5.1 方法调用的内存解析#
- 形参:方法声明时,一对小括号内声明的参数,简称:形参
- 实参:方法调用时,实际赋值给形参的值,称为:实参

过程概述:
每当调用一个方法时,方法就以栈帧的方法加载进虚拟机栈中。方法中声明的局部变量存放在栈帧中。
当方法执行结束时,栈帧就会弹出栈。栈帧中存放的局部变量也随之消亡。
5.2 目前为止,内存分析(重要)#
  • 基本原则

1、JVM中内存划分

  • 栈:以栈帧为基本单位(每个方法对应一个栈帧);栈帧里存放局部变量。
  • 堆:new 出来的"东西":数组实体(含数组元素)、对象实体(含成员变量)

2、区分清成员变量(类内部、方法外声明的)、局部变量(方法的形参、方法内定义的变量、构造器内定义的变量、构造器的形参、代码块内部等)

3、值传递机制:

  • 如果参数是基本数据类型,传递的是基本数据类型变量存储的数据值
  • 如果参数是引用数据类型,传递的是引用数据类型变量存储的地址值

6. 再谈方法#

6.1 方法的重载(overload )#
1. 定义:
在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。满足这样特点的多个方法彼此之间称为
方法的重载。

2. 总结为:"两同一不同":同一个类、相同的方法名;形参列表不同(参数的个数不同,参数的类型不同)
> 重载与否与形参名没有关系、返回值类型没有关系、权限修饰符没有关系

3. 举例
> Arrays中的重载的binarySearch(xxx) \ equals(xxx,xxx) \ toString(xxx)
> System.out的多个重载的println();

4. 如何判断两个方法是相同的呢?(换句话说,编译器是如何确定调用的某个具体的方法呢?)
> 在同一个类中,只要两个方法的方法名相同,且参数列表相同(参数的个数相同且参数类型相同),
   则认为这两个方法是相同的。
        > 与方法的权限修饰符、返回值类型、形参名都没有关系。

> 在同一个类,不能编写两个相同的方法的。

后续会讲:方法的重写(overwrite / override)

面试题:方法的重载与重写的区别?

throw \ throws
Collection \ Collections
final \ finally \ finalize
String \ StringBuffer \ StringBuilder
ArrayList \ LinkedList 
。。。
== 、equals()
抽象类、接口
6.2 可变个数形参的方法#
1. 使用场景
JDK5.0的新特性。

如果方法在调用时,参数的类型是确定的,但是参数的个数不确定,则可以考虑使用可变个数形参的方法。

2. 格式:类型 ... 变量名


3. 说明:
> 可变个数形参的方法在调用时,可以传入0个,1个或多个参数。
> 可变个数形参的方法与参数是其它类型的同名方法构成重载。
> 可变个数形参的方法与参数是同样类型的数组参数构成的方法,在方法名相同的情况下,不构成重载。即两个方法不能
  同时存在。
        > 可变个数的形参在编译器看来就是同一个类型的数组参数
> 规定:可变个数的形参需要声明在方法形参列表的最后
> 一个方法的形参位置,最多只能有一个可变个数的形参
/*
String sql1 = "update customers set name = ?,salary = ? where id = ?";
String sql2 = "delete from customs where id = ?";

public void update(String sql,Object ... objs){
	//使用可变形参objs中的各个元素值给形参sql中的?赋值
}
*/
6.3方法的参数传递机制(难点、重点)#
1. 对于方法内声明的局部变量来说:

> 如果此局部变量是基本数据类型的,则将基本数据类型变量保存的数据值传递出去
> 如果此局部变量是引用数据类型的,则将引用数据类型变量保存的地址值传递出去


2. 方法的参数的传递机制:值传递

2.1 概念(复习)
形参:方法声明时,一对小括号内声明的参数,简称:形参
实参:方法调用时,实际赋值给形参的值,称为:实参

2.2 规则
> 如果此形参是基本数据类型的,则将基本数据类型的实参保存的数据值传递给形参
> 如果此形参是引用数据类型的,则将引用数据类型的实参保存的地址值传递给形参


3. 面试题:Java中的参数传递机制是什么? 值传递机制。
6.4 递归方法(熟悉)#
1. 何为递归方法?
方法自己调用自己的现象就称为递归。


2. 递归方法分类
直接递归、间接递归。

3. 使用说明:
- 递归方法包含了一种`隐式的循环`。
- 递归方法会`重复执行`某段代码,但这种重复执行无须循环控制。
- 递归一定要向`已知方向`递归,否则这种递归就变成了无穷递归,停不下来,类似于`死循环`。最终发生`栈内存溢出`。

7. 对象数组(难点)#

1. 何为对象数组?如何理解?

数组中的元素,如果存储的是对象的话,则称此数组为对象数组。

2. 举例:
String[] arr = new String[10];
arr[0] = "hello";
arr[1] = new String("abc");

Person[] arr1 = new Person[10];
arr1[0] = new Person();

Phone[] arr2 = new Phone[10];

3. 内存解析:
数组名(比如:stus)存储在栈空间
创建的20个学生对象,存储在堆空间中。学生对象的地址值存储在数组的每个元素中。

8. 关键字:package、import#

  • package:包,指明了Java中的类、接口等结构所在的包。声明在文件的首行
  • import:导入。指明在当前类中使用的其它包中的结构。声明在package下,类的声明之前。
一、package关键字的使用

1. 说明
- package,称为包,用于指明该文件中定义的类、接口等结构所在的包。
- 一个源文件只能有一个声明包的package语句
- package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包。以后声明源文件时,不要使用无名包。
- 包名,属于标识符,满足标识符命名的规则和规范(全部小写)、见名知意
  - 包名推荐使用所在公司域名的倒置:com.atguigu.xxx。
  - 大家取包名时不要使用"`java.xx`"包,否则运行会报错
- 包对应于文件系统的目录,package语句中用 “.” 来指明包(目录)的层次,每.一次就表示一层文件目录。
- 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)。不同的包下可以定义同名的结构(类、接口)


2. 包的作用
- 包可以包含类和子包,划分`项目层次`,便于管理
- 帮助`管理大型软件`系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式
- 解决`类命名冲突`的问题 ---> 不同包下可以命名同名的类。
- 控制`访问权限` ---> 讲了封装性,大家就清楚了。


二、import关键字的使用
- import:导入,后面跟一个具体包下的类或接口等结构。

-为了使用定义在其它包中的Java类,需用import语句来显式引入指定包下所需要的类。
相当于`import语句告诉编译器到哪里去寻找这个类`。

- import语句,声明在包的声明和类的声明之间。

- 如果需要导入多个类或接口,那么就并列显式多个import语句即可

- 如果使用`a.*`导入结构,表示可以导入a包下的所有的结构。
  举例:可以使用java.util.*的方式,一次性导入util包下所有的类或接口。

- 如果导入的类或接口是java.lang包下的,或者是当前包下的,则可以省略此import语句。

- 如果已经导入java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。

- 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。

- (了解)`import static`组合的使用:调用指定类或接口下的静态的属性或方法

9. 面向对象的特征一:封装性#

  • 什么是封装性?
在Java实现项目时,将不用功能的代码封装进不同的方法。使用Java给我们提供的4种权限修饰对类及类的内部成员进行修饰。
体现被修饰的结构在调用时的可见性的大小。
  • 如何体现封装性?
> 举例1:类中的属性私有化,提供公共的get()和set()方法,用于获取或设置此属性的值。
> 举例2:如果类中存在一些方法,这些方法只在类的内部使用,不希望对外暴露,则可以将这些方法声明为私有的。
> 举例3:单例设计模式。(后面讲static的时候说)
  • 为什么需要封装性?
-  `高内聚`:类的内部数据操作细节自己完成,不允许外部干涉;
-  `低耦合`:仅暴露少量的方法给外部使用,尽量方便外部调用。


- 通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

10. 类的成员之三:构造器#

1. 构造器的理解

体会1: Scanner scan = new Scanner(System.in);
      Person per = new Person();

体会2:
construct : v. 建设、建造
construction: n. 建设、建造    CCB 中国建设银行  ICBC
constructor : n.建设者,建造者

2. 构造器的作用
>作用1:搭配new关键一起,用于对象的创建
>作用2:用于初始化对象中的成员变量

3. 构造器的使用说明
> 一个类中,如果没有显式提供构造器的话,则JVM会默认提供一个空参的构造器。(其权限修饰符与类的权限修饰符相同)
> 声明格式:权限修饰符 类名(形参列表){}
> 一个类的多个构造器,彼此构成重载
> 如果一个类中,一旦显式的声明了构造器,则JVM不再提供默认的空参的构造器了。
> 结论:凡是类,都有构造器(自始至终都是对的)

11. 其它几个小知识#

11.1 类中实例变量的赋值位置及顺序#
0.实例变量:属于属性(或成员变量),不使用static修饰即可。

1. 在类的属性中,可以有哪些位置给属性赋值?
> ① 默认初始化 ---> 只执行一次
> ② 显式初始化 ---> 只执行一次
> ③ 构造器中初始化  ---> 只执行一次
*********************************
> ④ 创建对象以后,通过"对象.属性" 或"对象.方法"的方式,给属性赋值  ---> 可以多次执行


2. 这些位置执行的先后顺序是怎样?
① - ② - ③ - ④



3. 以上操作在对象创建过程中可以执行的次数如何?
①、②、③:只执行一次
④:可以多次执行
11.2 JavaBean#
所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
11.3 UML类图#

理解

11.4 匿名对象#
//匿名对象
System.out.println(new Circle(2.5).findArea());
//知识点1:如上写法的匿名对象,只能被调用一次。
System.out.println(new Circle(2.5).getRadius());
//知识点2:开发中,常常将匿名对象作为参数传递给方法的形参。
Test4_5 test = new Test4_5();

test.show(new Circle(3.4));

第07章:面向对象-进阶#

1. 关键字:this#

  • this可以调用属性、方法;构造器。
    • 记住:this必须使用的场景:属性与形参同名时;调用重载的构造器

2. 面向对象特征二:继承性#

  • 为什么需要继承性?

    • 继承的出现减少了代码冗余,提高了代码的复用性。
    • 继承的出现,更有利于功能的扩展。
    • 继承的出现让类与类之间产生了is-a的关系,为多态的使用提供了前提。
  • 什么是继承性?

    • class B extends A{}

      继承中的基本概念:

      A类:父类、SuperClass、超类、基类
      B类:子类、SubClass、派生类

  • 继承性的基本使用

1. 有了继承性以后:
> 子类继承父类以后,就获取了父类中声明的所有的属性和方法。 ----> 刻画是否存在此属性、方法
  但是,由于封装性的影响,可能导致子类不能调用。  ----> 刻画能否调用此属性、方法

> extends: 继承。还可以理解为“扩展、延展”。意味着子类在继承父类的基础上,还可以扩展自己特有的属性、方法。
   父类、子类的关系不同于集合、子集的关系。


2. 默认的父类:
如果一个类显式声明了父类,则其父类为指定声明的父类。
如果一个类没有显式声明其父类,则默认继承于java.lang.Object类。

3. 补充说明:
> 一个父类可以被多个子类继承。
> 一个子类只能声明一个父类。----> Java中类的单继承性。
> Java中的类支持多层继承。
> 子类、父类是相对的概念。
    > 概念:直接父类、间接父类
> Java中的任何类(除了java.lang.Object类)都直接或间接的继承于java.lang.Object类。

3. 方法的重写#

1. 为什么需要方法的重写?

子类继承父类以后,父类中的方法在权限允许的情况下,子类可以直接调用。但是我们在一些场景中发现,父类
中的方法不适用于子类。怎么处理呢?需要使用方法的重写。

举例(银行账户):

class Account{ //账户
    double balance; //余额

    //取钱
    public void withdraw(double amt){
        if(balance >= amt){
            balance -= amt;
            System.out.println("取款成功");
        }
    }

    //...
}

class CheckAccount extends Account{  //信用卡账户
    double protectedBy; //可透支额度

    //取钱
    public void withdraw(double amt){
        if(balance >= amt){
            balance -= amt;
            System.out.println("取款成功");
        }else if(protectedBy >= amt - balance){
            protectedBy -= amt - balance;
            balance = 0;
            System.out.println("取款成功");
        }else{
            System.out.println("取款失败");
        }
    }
}


2. 何为方法的重写?
子类继承父类以后,对父类中继承过来的方法进行覆盖、覆写的操作。此操作就称为方法的重写。


3. 方法重写应遵循的规则

[复习]方法声明的格式:权限修饰符 返回值类型 方法名(形参列表){ 方法体 }

具体规则:称谓:父类被重写的方法;子类重写父类的方法
    > 子类重写父类的方法 与 父类被重写的方法的方法名、形参列表相同。
    > 子类重写父类的方法的权限修饰符不小于父类被重写的方法的权限修饰符
    > 返回值类型:
        > 父类被重写的方法的返回值类型为void,则子类重写父类的方法的返回值类型必须为void
        > 父类被重写的方法的返回值类型为基本数据类型,则子类重写父类的方法的返回值类型必须为同类型的基本数据类型
        > 父类被重写的方法的返回值类型为引用数据类型,则子类重写父类的方法的返回值类型与父类的相同,或是父类的类型的子类。

技巧:建议子类重写父类的方法时,我们将权限修饰符、返回值类型都声明为与父类的方法相同的。

注意点:
> 子类不能重写父类中声明为private权限的方法。

4. 面试题:区分方法的重载(overload)与重写(override / overwrite)

重载:"两同一不同"
重写:子类在继承父类以后,可以对父类中的同名同参数的方法进行覆盖、覆写。此操作即为方法的重写。
    具体的规则为:....。

4. 关键字:super#

  • super调用父类的属性、方法;构造器

    • 使用场景:子父类中出现同名属性;子类重写了父类的方法时。

      ​ super调用构造器,体现加载父类的结构。

5. 子类对象实例化的全过程(了解)#

1. 从结果的角度来看:---->体现为类的继承性。

当子类继承父类以后,子类就获取了父类(直接父类、所有的间接父类)中声明的所有的属性、方法。

当我们创建了子类对象以后,在堆空间中就保存了子类本身及其所有的父类中声明的属性。同时,子类对象在权限允许
的情况下,可以调用子类及其所有的父类中声明的方法。


2. 从过程的角度来看:

当我们通过子类的构造器创建对象时,一定会直接或间接的调用到其直接父类的构造器,其直接父类的构造器同样会
直接或间接的调用到其父类的构造器,...,以此类推,最终一定会调用到java.lang.Object类的构造器为止。

因为我们调用过所有的父类的构造器,进而所有的父类就需要加载到内存中,进而堆空间中就有所有父类中声明的属性。
以及可以在权限允许的情况下,调用父类中声明的方法。


问题:在创建子类对象的过程中,一定会调用父类中的构造器吗? yes!



3. 问题:创建子类的对象时,内存中到底有几个对象?
只有1个!

6. 面向对象特征三:多态性#

6.1 向上转型:多态#

  • Java中的多态性体现为:子类对象的多态性(狭义上理解)。即父类的引用指向子类的对象。
  • 应用场景:当通过父类的引用调用方法时,实际执行的是子类重写父类的方法。
  • 好处:多态性常使用在方法的形参位置。多态的出现,极大的减少了方法的重载,同时有利于程序的扩展。
  • 举例:① equals(Object obj) ② Account - Customer : setAccount(Account acct) ③ 凡是代码中出现了抽象类、接口,都可以体现为多态性。
  • 共识:Java中的多态性(广义上理解):1、子类对象的多态性。 2、方法的重写。

6.2 向下转型:多态的逆过程#

  • Student s = (Student)new Person(); //编译通过,运行不通过。
  • 如何向下转型:使用强转符:()
  • 可能出现的问题:可能会出现ClassCastException异常
  • 如何解决?建议在强转前进行instanceof的判断。

7. Object类的使用#

1. Object类的说明
> java.lang.Object类是所有Java类(除了自己以外)的根父类。
> java.lang.Object类中没有声明属性,声明有一个空参的构造器:Object(){}
  下面重点关注java.lang.Object类中声明的方法。在权限允许的情况下,任何一个类的对象都可以调用。

2. 常用方法
   重点方法:equals(Object obj) \ toString()
   熟悉方法:clone() \ finalize()
   目前不需要关注:getClass() \ hashCode() \ wait() \ wait(xxx) \ notify() \ notifyAll()

7.1 equals()方法#

区分 == 和 equals()

1. == : 运算符,适用于基本数据类型、引用数据类型
  equals():方法,适用于引用数据类型

2. 针对于引用数据类型, == :用来比较两个引用变量的地址值是否相等。(或判断两个引用是否指向同一个对象)
                    equals(): 需要区分此方法是否被重写过。具体见3

3.
3.1 像String、Date、包装类、File等类,它们都重写了Object类中的equals()方法,用于比较对象的实体内容
是否相等。如果相等,就返回true。

3.2 对于自定义的类,如果没有重写Object类中的equals()方法,则仍然比较两个对象的地址值是否相等。
  如果我们重写Object类中的equals()方法的话,通常也是用来比较两个对象的实体内容是否相等。
int i = 65;
int j = 65;
sout(i == j);//true

char c = 'A';
sout(i == c);//true

float f = 65.0F;
sout(i == f);//true

7.2 toString()方法#

1. Object类中toString()的定义:
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

2. 开发中的使用场景
> 像String、Date、包装类、File等类,它们都重写了Object类中的toString(),用于返回当前对象的实体内容。

> 对于自定义的类,如果没有重写Object类中的toString()方法,则仍然返回当前对象的类型及地址
  如果重写了Object类中的toString()方法,通常也都是返回当前对象的实体内容。

3. 开发中使用说明:

对于自定义的类,当我们调用对象的toString()方法时,习惯上希望输出对象的实体内容。所以,需要重写Object
类中的toString(),否则就是返回当前对象的类型及地址了。

8. 项目二:拼电商客户管理系统#

  • 项目中主要的类:
    • (重点)封装客户数据的类:Customer
    • (重点)管理多个客户对象的类:CustomerList
    • 与用户交互的管理界面操作的类:CustomerView
    • 封装Scanner的工具类:CMUtility

第08章:面向对象-高级#

1. 关键字:static#

2. static 用来修饰的结构:属性、方法;代码块、内部类

3. static修饰属性
   3.1 复习:变量的分类
   方式1:按照数据类型: 基本数据类型、引用数据类型

   方式2:按照类中声明的位置:
    成员变量(或属性):以是否使用static修饰
        > 使用static修饰 :  类变量(或静态变量)
        > 不使用static修饰 : 实例变量(或非静态变量)

    局部变量:方法内声明的变量、方法形参、构造器内声明的变量、构造器形参、代码块内声明的变量等。


   3.2 静态变量:类中的属性使用static进行修饰。
       对比静态变量与实例变量:
       ① 个数
        >静态变量:内存中只存在一份。与具体对象的个数,以及是否存在对象都无关。
        >实例变量:归属于具体的对象所有。进而创建过几个对象,就存在一个实例变量。

       ② 内存位置
        >静态变量:jdk6:存放在方法区。 从jdk7开始,存放在堆空间中。(注意:不在具体的对象内部)
        >实例变量:堆空间存储了对象实体。在具体的对象实体中,保存着实例变量。

       ③ 加载时机
        >静态变量:随着类的加载而加载。(即类加载完成时,此静态变量就分配好了内存空间)
        >实例变量:随着对象的创建,在堆空间此对象内部,分配内存存储具体的实例变量。

       ④ 调用者
        >静态变量:可以被类调用,也可以被类的对象调用。
        >实例变量:只能被类的对象调用

       ⑤ 判断是否可以调用 ---> 从生命周期的角度解释
                    类变量         实例变量
         类          yes             no
         对象        yes             yes
       ⑥ 消亡时机
        >静态变量:随着类的卸载而消亡。
        >实例变量:随着对象的消亡而消亡。



4. static修饰方法:(类方法、静态方法)
    > 随着类的加载而加载
    > 静态方法,可以使用"类.静态方法"的方式进行调用
      同时,还可以使用"对象.静态方法"的方式进行调用。--->从生命周期的角度解释
    > 判断是否可以调用
                          类方法         实例方法
               类          yes             no
               对象        yes             yes
    > 静态方法中只能调用当前类中的静态的变量、静态的方法。(即不能调用非静态的变量、非静态的方法)
      非静态的方法中既可以调用当前类中非静态的变量、非静态的方法,也可以调用静态的变量、静态的方法。

    > 静态方法中不能使用this、super关键字。

5. 开发中,什么时候需要将属性声明为静态的?

    > 是否适合被类的多个对象所共享,同时多个对象对应的此变量值是相同的。
    > 开发中,常常将一些常量声明为静态的。比如:Math的PI。


   什么时候需要将方法声明为静态的?

    > 操作静态变量的方法,通常设置为静态方法
    > 开发中,工具类中的方法常常设置为static的。

2. 单例模式#

1. 设计模式概述:
设计模式是在大量的`实践中总结`和`理论化`之后优选的代码结构、编程风格、以及解决问题的思考方式。
设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。


2. 何为单例模式(Singleton):在整个软件系统中,针对于某个类来讲,只存在该类的唯一的一个实例。则此类的设计
即为单例模式。

3. 如何实现单例模式(掌握):

饿汉式、懒汉式


4. 对比两种模式(特点、优缺点)
特点:
    饿汉式:随着类的加载,当前类的实例就创建成功。
    懒汉式:只有在首次调用get()方法时,才会创建单例对应类的实例。

饿汉式:(缺点)类一加载对象就创建成功,占用内存时间较长。(优点)线程安全的。
懒汉式:(优点)延迟了对象的创建,节省内存空间。(缺点)线程不安全的。 --->后续多线程章节中,将此方式改为线程安全的。

【面试题】 写一个(线程安全的)单例模式。

3. main()的理解(了解)#

1. 理解1:程序的入口。
   理解2:看做是一个普通的有形参的静态方法。

2. 与控制台交互(了解即可)

方式1:使用Scanner类及其内部的nextXxx()
方式2:使用main(),将从控制台获取的数据存储在其形参String[] args中。

4. 类的内部成员之四:代码块#

4.1 代码块的基本使用#

1. 代码块(或初始化块)的作用:用来对类或对象进行初始化操作的。

2. 代码块的修饰:只能使用static修饰。


3. 代码块的分类:静态代码块、非静态代码块


4. 具体使用:
4.1 静态代码块:
    > 随着类的加载而执行。主要用来初始化类。
    > 因为类只加载一次,进而静态代码块也只会执行一次
    > 内部可以有输出语句、声明变量等操作
    > 内部可以调用当前类中静态的结构(属性、方法),不能调用非静态的结构
    > 如果一个类中声明了多个静态代码块,按照声明的顺序先后执行
    > 静态代码块的执行要先于非静态代码块的执行

4.2 非静态代码块:
    > 随着对象的创建而执行。主要用来初始化对象。
    > 每创建一个对象,非静态代码块就执行一次。
    > 内部可以有输出语句、声明变量等操作
    > 内部可以调用当前类中静态的结构(属性、方法),能调用非静态的结构
    > 如果一个类中声明了多个非静态代码块,按照声明的顺序先后执行

4.2 属性赋值位置、过程#

1. 可以给类的非静态的属性(即实例变量)赋值的位置有:
① 默认初始化
② 显式初始化 / ③ 代码块中初始化
④ 构造器中初始化
***************************
⑤ 有了对象以后,通过"对象.属性"或"对象.方法"的方式给属性赋值


2. 执行的先后顺序:
① - ②/③ - ④ - ⑤

3. (超纲)关于字节码文件中的<init>\<clinit>的简单说明:

<clinit> : 系统自动生成的,内部包含了针对于静态属性的显式赋值、代码块中赋值操作。
           如果类中的静态属性没有显式赋值、没有静态代码块,则不会自动生成<clinit>方法。
           > 内部显式赋值、代码块中赋值操作的执行先后顺序取决于声明的先后顺序。

<init> : 系统自动生成的,内部包含了针对于非静态属性的显式赋值、代码块中赋值、构造器中赋值操作。
         > 一个字节码文件中至少包含一个<init>。换句话说,一个字节码文件中,包含几个<init>方法
           取决于类中声明了几个构造器。
         > 内部显式赋值、代码块中赋值操作的执行先后顺序取决于声明的先后顺序;构造器中赋值操作是最后执行的。

5. 关键字:final#

1. final的理解:最终的

2. final可以用来修饰的结构:类、方法、变量

3. 具体说明:

3.1 final修饰类:此类不能被继承。
        > 比如:String、StringBuffer、StringBuilder类都使用了final修饰。

3.2 final修饰方法:此方法不能被重写。
        > 比如:Object类中的getClass()

3.3 final修饰变量(重点关注):表示此变量一旦赋值就不可更改,即此变量理解为是一个常量。
    > final修饰成员变量:此变量即为一个常量。
            可以有哪些位置给常量赋值呢?① 显式赋值 ② 代码块中赋值 ③ 构造器中赋值。


    > final修饰局部变量:此变量即为一个常量。
          此局部变量只能被赋值一次。针对于形参来讲,使用final修饰以后,在调用此方法时给此常量形参赋值。

4. final与static搭配:用来修饰一个属性,此属性称为:全局常量。
比如: Math类中的PI。

6. 关键字:abstract#

1. abstract的概念:抽象的

2. abstract可以用来修饰:类、方法

3. 具体的使用:
    abstract修饰类:抽象类
        > 不能实例化!
        > 抽象类中一定声明有构造器,只是不能创建对象而已。---> 此时的构造器,用来给子类对象实例化时调用的。
        > 抽象类中的方法可以是抽象方法,也可以是普通的非抽象方法。


    abstract修饰方法:抽象方法
        > 不包含方法体的方法,并且使用abstract修饰。
        > 抽象类中可以没有抽象方法,但是抽象方法所属的类一定是抽象类。
        > 子类继承抽象父类以后,如果重写了父类中的所有的抽象方法,此子类方可实例化。
                            如果子类没有重写父类中所有的抽象方法的话,则此子类必须也声明为抽象类。

4. abstract不能使用的场景
4.1 abstract 不能修饰哪些结构?属性、构造器、代码块等

4.2 abstract 不能与哪些关键字共用?
不能用abstract修饰私有方法、静态方法、final的方法、final的类。


5. 注意:
抽象类在使用时,如果出现在方法的形参位置。则在调用方法时,一定要使用多态了。

7. 与类并列的结构:接口(interface)#

1. 定义接口的关键字: interface

2. 接口的理解:
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要...则必须能...”的思想。
继承是一个"是不是"的is-a关系,而接口实现则是 "能不能"的`has-a`关系。


3. 接口内部结构的说明:
   > 可以声明:
        jdk8之前:只能声明全局常量(public static final)、抽象方法(public abstract)
        ******************************************************
        jdk8中:增加了静态方法、默认方法(default)
        jdk9中:增加了私有方法。

   > 不可以声明:构造器、代码块等结构。


4. 接口与类的关系 :实现关系(implements)

5. 满足此关系之后,说明:
> 实现类实现相应的接口以后,就获取了接口中声明的全局常量和抽象方法。
> 如果实现类重写了接口中声明的所有的抽象方法,则此实现类可以实例化
  如果实现类没有重写完接口中声明的所有的抽象方法,则此实现类仍为一个抽象类。
> 一个类可以实现多个接口。--->一定程度上缓解了Java中类的单继承性的局限性。


6. 格式:
class SubA extends SuperA implements A,B,C{}


7. 接口与接口的关系:继承关系,而且是多继承的。
interface A{
    void method1();
}
interface B{
    void method2();
}
interface C extends A,B{} //多继承

8. 接口的多态性(重要)


9. 面试题:区分抽象类和接口
角度1:
共性:都不能实例化
不同点:抽象类:有构造器
       接口:没有构造器

角度2:抽象类中可以声明抽象方法;接口中(jdk8之前)方法只能是抽象的。

角度3:类与类之间是继承关系,是单继承的;接口与接口之间是继承关系,是多继承的;类与接口之间是实现关系,是多实现的。

角度4:jdk8及之后的新特性:接口中可以声明静态方法、默认方法,包含方法体。
      jdk9:新增私有方法。

8. 类的内部成员之五:内部类#

> 内部类的分类(参照变量的分类)
> 如何创建成员内部类的对象
> 从两个角度来认识成员内部类(作为类、作为外部类的成员)
> 内部类如何调用外部类的成员(属性、方法)
    > 在出现同名的属性、方法时,使用"外部类.this.结构"的方式显式调用父类的结构。
> 谈谈局部内部类开发中的使用场景

9. 枚举类#

> 枚举类的特点:一个类中的对象个数是有限的、可数个的。
> (了解)jdk5.0之前,枚举类的定义方式。
> jdk5.0中新增了enum的方式定义枚举类。 ----需要掌握
> 自定义的枚举类的父类:Enum类。此类中声明的常用方法。
	> values() \ valueOf(String objName) \ toString() ; name() \ ordinal()
> 枚举类实现接口。

10. 注解#

> 注解的作用:与注释的区别。注解的作用
> Java基础中三个常见的注解
> 如何自定义注解
> 元注解:对现有的注解进行修饰作用的注解。
> 体会:框架的理解:框架 = 注解 + 反射 + 设计模式

> 掌握如何使用:单元测试方法。

11. 包装类#

> 理解:为什么需要包装类?
	> add(Object obj) / equals(Object obj)
> 基本数据类型以及对应的包装类
> 重点:基本数据类型、包装类、String三者之间的转换
	> 基本数据类型 < --- > 包装类:自动装箱、自动拆箱
	> 基本数据类型、包装类 ---> String: 调用String的valueOf(); + 
	> String ---> 基本数据类型、包装类:调用包装类的parseXxx(String str)

12.IDEA的使用#

  • IDEA常用的快捷键
  • IDEA的debug功能

第09章:异常处理#

1. 异常的概述、理解#

1. 什么是异常?
指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。

2. 异常的抛出机制 ---> 万事万物皆对象
Java中把不同的异常用不同的类表示,一旦发生某种异常,就`创建该异常类型的对象`,并且抛出(throw)。
然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常
对象将会导致程序终止。

3. 如何对待异常
> 态度1:一是遇到错误,不进行任何的处理,终止程序的运行。
> 态度2:如果之前的测试中出现了异常的情况,则修改代码,保证之后尽量不要出现同样的异常。
> 态度3:在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,
        要编写相应的代码进行异常的检测、以及`异常的处理`

2. 常见的异常(重点)#

java.lang.Throwable
    |---java.lang.Error:错误
            > Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。
            > 一般不编写针对性的代码进行处理。
            > 常见的Error:StackOverFlowError,OutOfMemoryError

    |---java.lang.Exception:异常
            > 其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,
               使程序继续运行。
            > 分类:编译时异常 、 运行时异常
            > 举例:
                    运行时异常:
                    NullPointerException
                    ArrayIndexOutOfBoundsException
                    ClassCastException
                    NumberFormatException
                    ArithmeticException
                    InputMismatchException
                    编译时异常:
                    ClassNotFoundException
                    FileNotFoundException
                    IOException

3. 异常的处理(重点)#

3.1 try-catch-finally#
  • try-catch的使用
1. 方式一(抓抛模型):

过程1:“抛”:Java程序的执行过程中如果出现异常,会生成一个对应异常类的对象,并将此对象抛出。


过程2:“抓”:针对于上一个过程中抛出的异常类的对象,进行的捕获(catch)行为。


2. 基本结构:
try{
    //可能出现异常的代码

}catch(异常类型1 e){
    //异常的处理方式
}catch(异常类型2 e){
    //异常的处理方式
}
...
finally{
    //一定会被执行的代码
}


3. 使用细节:
> finally是可选的。暂时先不考虑
> try中包裹的是可能出现异常的代码。如果在执行过程中,没有出现异常,则程序正常结束,不会考虑执行多个catch结构
  如果try中出现了异常,则会自动创建对应异常类的对象,并将此对象抛出。
        如果抛出的异常对象匹配某个具体的catch结构,则进入相应的catch中进行处理。一旦执行结果,就跳出当前结构,继续执行其后的代码
        如果没有匹配到相应的catch结构,则相当于没有捕获异常,会导致程序的终止。
> 如果多个catch中的异常类型有子父类关系,则必须将子类异常的捕获声明在父类异常捕获的上面。
> try中声明的变量,在出了try的一对{}之后,就失效了。
> catch中异常处理的方式:
        方式1:自己自定义输出语句
        方式2:调用异常类的现有方法:方法1:调用printStackTrace(),用于打印异常出现的堆栈信息。(推荐)
                               方法2:调用getMessage(),返回一个异常的字符串信息


4. 开发体会:
   > 对于运行时异常:实际开发中,我们通常都不再处理运行时异常。

   > 对于编译时异常:实际开发中,我们是必须要提前给出异常方案。否则,编译不通过。
  • finally的使用
1. finally的理解
> 将一定会被执行的代码声明在finally中
> finally结构是可选的。
> 不管try、catch中是否存在未被处理的异常,不管try、catch是否执行了return语句;finally是一定要被执行的结构。

2. 什么样的代码我们一定要声明在finally中呢?
> 开发中会涉及到相关的资源(流、数据库连接)的关闭的问题,如果相关的资源没有及时关闭,会出现内存泄漏。
  为了避免出现内存泄漏,我们必须将其关闭操作声明在finally中,确保在出现异常的情况下,
  此关闭操作也一定会被执行。
3. 面试题
final 、 finally 、finalize 的区别
3.2 throws#
1. 格式:
在方法的声明处,使用"throws 异常类型1,异常类型2,..."

2. 举例:
public void method1() throws FileNotFoundException, IOException{}

3. 是否真正处理了异常?

> 从是否能通过编译的角度来说:使用throws的方法声明了可能出现的异常的类型,使得编译能通过。
> 从是否真正意义上解决了可能抛出的异常对象:并没有。只是将可能出现的异常对象继续向上抛出。
                                   只有使用try-catch-finally的方式才是真正意义上处理了异常。


4. 方法的重写的要求:

针对于编译时异常:
子类重写父类中的方法,要求子类重写的方法抛出的异常类型不大于父类被重写方法抛出的异常类型。
比如:父类被重写的方法throws 异常类型1,子类可以throws异常类型1或异常类型1的子类。

针对于运行时异常:没有这样的要求。  ----> 开发中,针对于运行时异常,也不会进行try-catch或throws的处理。
  • 如何选择两种处理方式?
开发中,如何选择异常处理的两种方式?(重要、经验之谈)

> 情况1:如果程序中使用了相关的资源,为了确保这些资源在出现异常的情况仍然能被执行关闭操作。
        建议使用:try-catch-finally。将资源的关闭操作声明在finally中。

> 情况2:如果父类中被重写的方法没有使用throws的结构,则子类重写父类的方法中如果出现编译时异常,只能
        使用try-catch-finally的方式进行处理。

> 情况3:如果在方法1中依次调用了方法2,3,4,而且此时的方法2,3,4是递进调用的关系。则通常方法2,3,4中
        出现异常的情况下,选择使用throws的方式进行异常的处理,在方法1中使用try-catch-finally进行处理。

4. 手动抛出异常的对象(熟悉)#

1. 为什么需要手动抛出异常?

在实际开发中,为了满足实际问题的需要,必要时需要手动的throw一个异常类的对象。
比如:要求分子、分母都不能为负数。如果出现负数了就报错。如何体现报错呢?手动抛出异常类的对象。
比如:给学生的id赋值,要求此id不能为负数。如果赋值为负数,就报错。如何体现报错呢?手动抛出异常类的对象。


2. 如何理解"自动 vs 手动"抛出异常对象?

过程1:“抛”:Java程序的执行过程中如果出现异常,会生成一个对应异常类的对象,并将此对象抛出。
    情况1:自动抛出(throw)

    情况2:手动抛出(throw):在方法体内使用


过程2:“抓”:针对于上一个过程中抛出的异常类的对象,进行的捕获(catch)行为。
    广义上"抓"理解为异常处理的方式:
        > 方式1:try-catch-finally
        > 方式2:throws:使用在方法的声明处

类比:上游排污、下游治污。

3. 如何实现手动抛出异常?
在方法体的内部,满足某个条件的情况下,使用“throw + 异常类的对象”。


4. 注意点:throw后的代码不能被执行,编译不通过。

[面试题] 区分throw和throws

5. 如何自定义异常类(熟悉)#

1. 如何自定义异常类? (参照着Exception、RuntimeException进行设计即可)
① 继承于现有的异常体系结构中的某一个类。比如继承于RuntimeException、Exception
② 提供几个重载的构造器
③ 提供一个全局常量serialVersionUID,用于唯一的标识当前类


2. 如何使用自定义异常类?
在满足相应情况的条件下,方法体内使用"throw + 自定义异常类的对象"的方式使用。


3. 为什么需要自定义异常类?

我们在开发中,针对于出现的异常,比较关心的是异常的名称。通过异常名,就可以直接定位出现的异常的问题。
所以,我们在开发中,具体到项目的具体要求时,我们都可以抛出自己定义的异常类型的对象。

五个关键字:

try-catch-finally

throws

throw

第10章:多线程#

1. 相关概念#

  • 掌握:程序、进程、线程
  • 熟悉:线程的调度机制:分时调度、抢占式调度
  • 了解:单核CPU、多核CPU
  • 了解:并行与并发

2. 创建多线程的两种经典方式(重点)#

1. 线程的创建方式一:
1.1 步骤:
① 创建一个继承于Thread类的子类
② 重写Thread类的run()方法:将此线程要执行的操作编写在此方法体中。
③ 创建Thread类的子类的对象
④ 调用start()方法: 1、启动线程 2、调用线程的run()


1.2 例题:创建一个分线程1,用于遍历100以内的偶数
【拓展】 再创建一个分线程2,用于遍历100以内的偶数


2. 线程的创建方式二:
2.1 步骤:
① 创建实现Runnable接口的实现类
② 实现接口中的抽象方法run():将此线程要执行的操作编写在此方法体中。
③ 创建此实现类的对象
④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
⑤ 通过Thread类的对象调用start():1、启动线程 2、调用线程的run()



2.2 例题:创建分线程遍历100以内的偶数


3. 对比两种方式?
   共同点:① 创建的线程都是Thread类或其子类的对象
         ② 启动线程,调用的都是Thread类中的start()

   不同点:一种是继承的方式,一种是实现的方式(推荐);
        推荐实现的方式的原因: ① 类的单继承的局限性 ②实现的方式更适合、方便的用来处理共享数据的场景。

   联系:
        public class Thread implements Runnable

3. 线程的常用方法、生命周期#

一、线程的常用结构
1. 线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2.线程中的常用方法:
> run():在继承的方式中,需要被重写的方法。
> start():要想启动一个线程,必须要调用此方法:①启动线程 ② 调用线程的run()
> static currentThread():获取当前执行的代码所属的线程。
> getName():获取线程名
> setName(String name):设置线程名
> yield():一旦线程执行此方法,当前线程就释放cpu的执行权
> join(): 在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b执行结束以后,线程a才可以从被阻塞的位置继续执行
> static sleep(long millis):指定线程"睡眠"多少毫秒
> isAlive() : 判断当前线程是否存活

过时方法:
> stop():强行结束一个线程的执行,直接进入死亡状态。
> suspend() / resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。


3. 线程的优先级:
3.1 线程的优先级的范围:[1,10]
int MAX_PRIORITY = 10; //最大优先级
int MIN_PRIORITY = 1; //最小优先级
int NORM_PRIORITY = 5; //默认优先级

3.2 如何设置/获取优先级:
setPriority(int priority):设置线程的优先级
getPriority() : 获取线程的优先级

4. 线程的安全问题与同步机制(重点)#

线程的安全问题与线程的同步机制

1. 多线程卖票,出现的问题:出现了重票、错票

2. 什么原因导致的?一个线程在没有操作完ticket的情况下,其他线程参与进来,导致出现了重票、错票

3. 如何解决?
  应该包装一个线程在操作完共享数据ticket的情况下,其它线程才能参与进来继续操作ticket。

4. Java是如何解决线程的安全问题的? 同步机制

方式1:同步代码块

synchronized(同步监视器){
    //需要被同步的代码
}

说明:
> 需要被同步的代码,即为操作共享数据的代码。
> 什么是共享数据:即为多个线程共同操作的数据。比如:ticket
> 使用synchronized将操作共享数据的代码包起来,确保这部分代码作为一个整体出现。只有当一个线程操作完此部分代码
  之后,其他线程才有机会操作同样的这部分代码。
> 同步监视器,俗称锁。哪个线程获取了同步监视器,这个线程就能执行操作共享数据的代码。没有获取同步监视器的线程就只能等待。

注意:
> 操作共享数据的代码,不能包多了,也不能包少了。
> 同步监视器:任何一个类的对象,都可以充当同步监视器。但是,多个线程必须共用同一个同步监视器。
> 实现Runnable的方式中,使用的同步监视器可以考虑this。
  继承Thread类的方式中,使用的同步监视器慎重this,可以考虑使用当前类。

方式2:同步方法
如果操作共享数据的代码完整的声明在一个方法中。我们也可以考虑将此方法声明为同步方法。

说明:
> 非静态的同步方法,其默认的同步监视器是:this
> 静态的同步方法,其默认的同步监视器是:当前类本身

5. synchronized好处:解决了线程的安全问题
   弊端:串行的执行,是得多线程的性能受限

5. 同步机制的相关问题#

5.1 解决懒汉式的线程安全问题(重点)#
package com.atguigu04.threadsafemore.singleton;

/**
 * ClassName: BankTest
 * Description:
 *
 * @Author 尚硅谷-宋红康
 * @Create 2023/2/24 11:50
 */
public class BankTest {
    static Bank b1 = null;
    static Bank b2 = null;
    public static void main(String[] args) {


        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        });


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b2 = Bank.getInstance();
            }
        });

        t1.start();
        t2.start();


        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1 == b2);

    }
}


//懒汉式
class Bank{

    private Bank(){}

    private static Bank bank = null;

    //方式1:使用同步方法
//    public static synchronized Bank getInstance(){
//
//        if(bank == null){
//
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//
//            bank = new Bank();
//        }
//        return bank;
//
//    }

    //方式2:使用同步代码块
    public static Bank getInstance(){

        synchronized (Bank.class) {
            if(bank == null){

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                bank = new Bank();
            }
            return bank;
        }
    }

    //思考:使用同步代码块,存在指令重排
//    public static Bank getInstance(){
//
//        if(bank == null){
//
//            synchronized (Bank.class) {
//                if(bank == null){
//
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//
//                    bank = new Bank();
//                }
//
//            }
//        }
//
//        return bank;
//    }

}
5.2 死锁问题#
线程的同步机制带来的问题:死锁

1. 如何看待死锁?
> 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
> 一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
> 我们编程中,要避免出现死锁

2. 诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待

以上4个条件,同时出现就会触发死锁。


3. 如何避免死锁?
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
5.3 JDK5.0新增解决安全问题的方式:Lock#
除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁的方式

1. 步骤:
步骤1. 创建ReentrantLock的实例,必须保证多个线程共用一个。
步骤2. 调用lock(),锁住共享数据的代码
步骤3. 调用unlock(),解锁共享数据的代码

2. 面试题:
synchronized同步的方式 与Lock的对比 ?
    > synchronized同步机制,利用同步监视器,确保同步监视器的唯一性。
        > 同步代码块、同步方法对应的一对{}中的代码是需要被同步的,只能有一个线程执行。
    > Lock,确保Lock的实例的唯一性
        > 在lock()和unlock()方法之间的操作,确保只有一个线程在执行。


官方文档:
Lock implementations provide more extensive locking operations
than can be obtained using synchronized methods and statements.

6. 线程的通信#

1. 线程间的通信

为什么需要线程间的通信?
当我们`需要多个线程`来共同完成一件任务,并且我们希望他们`有规律的执行`,那么多线程之间需要一些通信机制,
可以协调它们的工作,以此实现多线程共同操作一份数据。

2. 涉及到三个方法的使用:
wait(): 一旦执行此方法,对应的线程就进入阻塞状态,并释放同步监视器的调用
notify():唤醒被wait的线程中优先级最高的那一个。如果被wait的多个线程优先级相同,则会随机唤醒其中被wait的线程。
notifyAll():唤醒所有被wait的线程。


3. 注意点:
> 此三个方法的调用者必须是同步监视器
> 此三个方法声明在java.lang.Object类中
> 此三个方法的使用,必须在同步方法或同步代码块中。
    ---> 在Lock方式解决线程安全问题的前提下,不能使用此三个方法。在Lock的情况下,使用Condition实现通信。


4. 案例:
案例1:使用两个线程打印 1-100。线程1, 线程2 交替打印

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


5. 【面试题】wait() 和 sleep()的区别?
> 所属的类:wait()存在于Object类的非静态方法;sleep()存在于Thread类的静态方法
> 使用环境:wait() 必须使用在同步代码块或同步方法中;sleep():在调用时没有任何限制
> 都使用在同步代码块或同步方法情况下,区别:wait()一旦调用会释放同步监视器;sleep()不会释放同步监视器
> 相同点:二者一旦执行都可以使得当前线程进入阻塞状态
    > 但是结束阻塞的方式不同:wait()的线程需要被notify()/notifyAll();sleep()的线程在指定时间结束后就结束阻塞。

7. JDK5.0新增两种创建多线程的方式#

  • 使用Callable接口
  • 使用线程池
1. 创建多线程的方式三:实现Callable (jdk5.0新增的)

与之前的方式的对比:对比Runnable
> call()方法有返回值类型,比run()灵活
> call()声明有throws结构,内部有异常的话,不必非要使用try-catch
> Callable接口使用了泛型,call()的返回值类型更加灵活。(超纲)



2. 创建多线程的方式四:使用线程池

此方式的好处:
> 提高了程序执行的效率
> 提高了资源的重用率
> 设置相关的参数,实现对线程的管理

联想:与后续讲的数据库连接池的好处是相同的。
  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值