✨写在前面
- 💖作者简介:大家好,我是 kitty_Happy。
- 😉正在学习的小白一名,很乐于与大家交流各种技术,共同学习!
- 🐈作者主页:kitty_Happy
- 🎉点赞 ➕ 评论 ➕ 收藏 == 养成习惯😜
- 文章前三章单独链接:4600字的封装、继承,多态笔记(推荐收藏)
- 💬 总结:希望你看完之后,能对你有所帮助,不足请指正! 🖊
- ✉️ 不要被任何人打乱自己的脚步,因为没有谁会像你一样清楚和在乎自己的梦想。♦
文章目录
🍖1、对象和封装
🍟1.1、构造方法
语法:
[ 访问修饰符 ] 方法名 ( [ 参数列表 ] ){
//……省略方法体的代码
}
-
构造方法的名称必须和类名相同,但是没有返回值类型。在构造方法的定义中参数列表是可选的,因此可以定义无参构造方法,也可以定义带参构造方法。
-
在java中,每一个类都默认自带了一个构造方法叫做缺省构造器,如有没有创建,则系统会自动创建一个默认的构造方法,默认构造方法是没有参数的,而且在方法体中没有代码。(无参构造器)
注意: 当我们创建了构造方法之后,Java自带的无参构造方法就会消失,需要我们自己写出来
🍟1.2、this
this 的三种用法:
1、使用 this 关键字调用成员变量,解决成员变量和局部变量的同名冲突
public Strive(String name){
this.name = name; //成员变量和局部变量同名,必须使用 this 关键字
}
2、使用 this 关键字调用成员方法
public Strive(String name){
this.name = name;
this.print(); // 调用成员方法
}
3.使用 this 关键字调用已定义的构造方法
在以下代码中定义了 Strive 类,并定义了两个带参构造方法
public void Strive(){
String name;
String sex;
int age;
String dept;
public Strive(String name, String sex){
this.name = name;
this.sex = sex;
}
public Strive(String name, String sex, int age, String dept){
this(name,sex); //调用已定义的构造方法(调用上面那个构造方法)
this.age = age;
this.dept = dept;
}
}
以上使用 this 关键字调用构造方法,并传入局部变量 name 和 sex 作为参数,执行时,首先执行 Strive(String name,String sex) 构造方法,完成对 name 属性和 sex 属性的赋值,然后执行后面的语句实现对 age 属性和 dept 属性的赋值。
🍟1.3、方法重载
🍕1.3.1、方法重载的定义
方法重载指的是同一个类包含两个或两个以上的方法,它们的方法名相同,方法参数个数不同,
方法参数类型不同。成员方法和构造方法都可以进行重载
例如:
System.out.println("good");
System.out.println(true);
System.out.println(100);
这里的方法重载使用的是方法参数类型不同
🍕1.3.2、方法重载的特点
方法重载判断的依据如下:
- 必须在同一个类中
- 方法名相同
- 参数列表 (方法参数的个数或参数类型) 不同
注意: 方法的返回值和方法的访问修饰符不能作为判断方法之间是否构成重载的依据
🍕1.3.3、方法重载的使用和优点
public int Strive(int a,int b){
return a+b;
}
public double Strive(double a,double b){
return a+b;
}
public String Strive(String s1,String s2){
return s1+s2;
}
public static void main(String[] args) {
Adder adder = new Adder();
int a = 6,b = 8;
System.out.println(adder.Strive(a,b));
double c = 9 , d = 10.9;
System.out.println(adder.Strive(c,d));
String s1 = "Hello",s2 = "World";
System.out.println(adder.Strive(s1,s2));
}
- 由以上代码可见,方法重载是对原有方法的一种升级,可以根据参数的不同,采用不同的实现方法,而且不需要编写多个名称,简化了类调用方法的代码
🍟1.4、使用封装重构类的属性和方法
🍕1.3.1、封装的概念
封装是面向对象的三大特性之一,就算将类的状态信息隐藏在类内部,不允许外部程序直接访问,而通过该类提供的方法实现对隐藏信息的操作和访问。
封装反映了事物的相对独立性,有效避免了外部错误对此对象的影响,并且能够给用户由于大意产生的错误操作起到预防作用
- 好处: 封装的好处在于隐藏类的细节,让用户只能通过开发人员规定的方法访问数据,可以方便地加入访问修饰符来限制不合理操作
🍕1.4.2、封装的步骤
实现封装的步骤如下:
1、修改属性的可见性来限制对属性的访问
2、 为每个属性创建一对赋值(setter)方法和取值(getter)方法,用户对这些属性的存取
3、在赋值方法中,可以加入对属性的存取控制语句
🧀1、修改属性的可见性:
使用 private 访问修饰符修饰,private 修饰的属性和方法只能在定义它的类的内部被访问
🧀2、创建赋值和取值方法:
get 方法取值、 set 方法存值
get 方法读、 set 方法改
以上看哪一个能理解,意思一致,代码如下:
private String name; // 私有属性
public void getName(){ //get方法 读
return name;
}
public void setName(String name){ //set 改
this.name = name;
}
🧀3、加入对属性的存取控制语句
为了限制不合理赋值,除了设置属性的私有性,还可以在赋值方法中加入对属性的存取控制语句。
假如性别:只有男,女,年龄:0~100 ;代码如下:
private grnder; // 性别
private int age; // 年龄
// 省略get 方法
public void setAge(int age){ //控制年龄
if(age < 0 || age > 150){
System.out.println("输入的年龄为:"+age+",该年龄不合法!");
return;
}else{
this.age = age;
}
}
public void setGender(String gender) { //控制性别
if(gender.equals("男") || gender.equals("女")){
this.gender = gender;
}else{
System.out.println("*** 性别不合法 !***");
}
}
🍟1.5、类和类成员的访问控制
1、类和访问修饰符
修饰符 | 同一包中 | 非同一包中 |
---|---|---|
public | 可以使用 | 可以使用 |
默认修饰符 | 可以使用 | 不可以使用 |
省略访问修饰符,就算默认修饰符
2、类成员的访问修饰符
修饰符 | 同一类中 | 同一包中 | 子类中 | 外部包 |
---|---|---|---|---|
private | 可以使用 | 不可以使用 | 不可以使用 | 不可以使用 |
默认修饰符 | 可以使用 | 可以使用 | 不可以使用 | 不可以使用 |
protected | 可以使用 | 可以使用 | 可以使用 | 不可以使用 |
public | 可以使用 | 可以使用 | 可以使用 | 可以使用 |
🍟1.6、static 关键字
static 关键字可以修饰类的属性,方法和代码块。使用 static 修饰的属性和方法不再属于具体的某个对象,而属性它所在的类
使用 static 修饰的 属性或方法可以使用 “ 类名 . ” 的方式调用,不需要再消耗资源反复创建对象
使用 static 修饰的方法属于 静态方法,使用类名调用,static 修饰的变量属于静态变量,类加载的时候加载进方法区
🍕1.6.1、用 static 关键字修饰属性和代码块
如下静态代码块:
static{
System.out.println("HelloWorld");
}
static 修饰的是类加载的时候执行,创建对象的时候执行,创建对象的时候应先执行 static静态代码块里面的内容
注意:
- 有多个静态代码块的时候,按照静态代码块的前后顺序执行
- 在方法里不可以定义 static 变量,也就是静态变量不能是局部变量
- 在静态方法中不能直接访问实例变量和实例方法
- 在实例方法中可以直接调用类中定义的静态变量和静态方法
🍖2、继承
🍟2.1、继承的基本概念
继承是面向对象的三大特征之一,继承可以解决编程中代码冗余的问题,是实现代码重用的重要手段之一。
语法:
[ 访问修饰符 ] class <SubClass> extends <SuperClass>{}
其中,SubClass 被称为子类或派生类,SuperClass 被称为父类或基类。
Java 继承规则如下:
1、可以继承父类中 public 和 protected 修饰的属性和方法,不论子类和父类是否在同一包中。
2、可以继承默认访问修饰符修饰的属性和方法,但是子类和父类必须在同一包中
3、无法继承 private 修饰的属性和方法
4、无法继承父类的构造方法
注意:
在 Java 中只支持单继承,即每个类只能有一个直接父类
🍟2.2、继承的应用
如下:
/**
*动物父类
*/
public class Animal {
public void cry(){
System.out.println("动物叫。。。。");
}
}
/**
* 继承了动物类的小猫类也有 cry() 方法(子类)
*/
public class Cat extends Animal {
// 有父类的方法与属性
}
🍟2.3、Object
Object 类属性超级父类(老祖宗),当一个类没有任何继承的时候,默认继承 Object 类,自带 Object 类里面的 方法与属性
注意:
子类被创建对象的时候必是先执行 Object 类的构造方法,因为构造方法第一行中有隐藏的 super() 调用父类构造方法,最终的父类一定是 Object 类
🍟2.4、继承关系中的方法重写
子类通过继承可以拥有和父类相同的特征和行为,另外,子类也可以定义自己特有的行为,既沿袭了父类的方法名称,又重新实现了父类方法,这就是方法重写。
在子类中可以根据需求对从父类继承的方法进行重写编写,这被称为方法重写或方法覆盖。
方法重写必须遵守以下规则:
- 重写方法和被重写方法必须具有相同的方法名
- 重写方法和被重写方法必须具有相同的参数列表
- 重写方法的返回值类型必须和被重写方法的返回值类型相同或是其子类
- 重写方法不能缩小被重写方法的访问权限
重写的时候可以在方法上面使用注解:
@Override
@Override 是用 Java 注解的方法表示该方法重写了父类方法,可以写也可以不写,在功能实现上没有区别,但是通过 @Override 注解,程序更加方便阅读。另外,编译器也会帮助验证 @Override 下面的方法名是否是父类所有的。如果其不符合方法重写规则,则会报错。
提示:
Java 注解又被称为 Java 标注,是 Java 5 引入的一种注解机制。
🍟2.5、方法重载和方法重写的区别
- 方法重载涉及同一个类中的同名方法,要求方法名相同,参数列表不同,与返回值类型和访问修饰符无关
- 方法重写涉及的是子类和父类之间的同名方法,要求方法名相同,参数列表相同,返回值类型i相同或是其子类
🍟2.6、super关键字
如果想在子类中调用父类的被重写的方法,可以使用 ” super.方法名 “实现
super 关键字代表对当前对象的直接父类对象的默认引用。在子类中可以通过 super 关键字访问父类的成员,包括父类的属性和方法。语法如下:
语法:
访问父类构造方法 :super(参数)
访问父类属性 / 方法 :super.< 父类属性 / 方法 >
使用 super 关键字,需要注意以下几点:
- super 关键字必须出现在子类(子类的方法和构造方法)中,而不允许在其他位置。
- 可以访问父类的成员,如父类的属性,方法,构造方法。
- 注意访问权限的限制,如无法通过 super 关键字访问 private 成员。
注意:
1、在构造方法中如果有 this 语句或 super 语句,则只能是第一条语句。
2、在一个构造方法中,不允许同时使用 this 关键字和 super 关键字调用构造方法(否则就有两条第一条语句)。
3、在静态方法中不允许出现 this 关键字或 super 关键字。
4、在实例方法中,this 语句和 super 语句不要求是第一条语句,可以共存。
5、子类构造方法中第一行有隐藏的 super( ) 调用父类构造方法,最终父类一定是 Object 类
🍟2.7、继承关系中的构造方法
在 Java 中,一个类的构造方法在如下两种情况下会被执行:
- 创建该类对象(实例化)
- 创建该类的子类对象(子类的实例化)
子类在实例化时,会首先执行其父类的构造方法,然后才会执行子类的构造方法。
在 Java 语言中,当创建一个对象时,Java 虚拟机(JVM)会按照父类——>子类的顺序执行一系列的构造方法。
子类继承父类时构造方法的调用规则如下:
- 如果在类的构造方法中没有通过 super 关键字显式调用父类地带参构造方法,也没有通过 this 关键字显式调用自身地其他构造方法,则系统会默认先调用父类的无参构造方法。在这种情况下,是否写 “ super( ); ”语句,效果是一样的。
- 如果在子类的构造方法中通过 super 关键字显式地调用了父类地带参构造方法,那么将执行父类相应的构造方法,而不执行父类无参构造方法。
- 如果在子类的构造方法中通过 this 关键字显式地调用了自身地其他构造方法,那么在相应构造方法中遵循以上两条规则。
- 如果存在多级继承关系,则在创建一个子类对象时,以上规则会多次向上更高一级父类应用,直到执行顶级父类 Object 类的无参构造方法为止。
🍖3、多态
面向对象的三大特性为 封装、继承、多态。最后一个特性——多态。它能使同一个操作当作用于不同的对象时,产生不同的执行结果。
使用多态可以提高代码的可维护性和可扩展性
🍟3.1、子类到父类的转换(向上转型)
子类到父类的转换被称为向上转型。(自动类型转换)
语法:
< 父类型 > < 引用变量名 > = new < 子类型 > ( );
Strive strive1 = new s1();
Strive 为父类型 strive1 为引用变量名 s1 为子类型
- 父类型的引用指向子类型的对象
实现多态的三个条件如下:
1、继承的存在(继承是多态的继承,没有继承就没有多态)。
2、子类重写父类的方法(多态下调用子类重写后的方法)。
3、父类引用变量指向子类对象(向上转型)。
🍟3.2、父类到子类的转换(向下转型)
父类到子类的转换被称为向上转型。(强制类型转换)
语法:
< 子类型 > < 引用变量名 > = ( < 子类型 > ) < 父类型的引用变量 >;
Strive strive1 = (Strive)s1;
s1 为 父类型的引用变量,Strive 为子类型,strive1 为引用变量名
🍟3.3、instanceof 运算符
语法:
对象 instanceof 类或接口
- 该运算符用来判断一个对象是否属于一个类或实现了一个接口,结果为 true 或 false。
- 在强制类型转换之前通过 instanceof 运算符检查对象的真实类型,再进行相应的强制类型转换,这样就可以避免类型转换异常,从而提高代码的健壮性。
if(Strive instanceof s1){ // 类型判断
Strive strive1 = (Strive)s1;
}else{
System.out.println("Strive与s1没有关系");
}
🍟3.4、多态的优势
- 可替换性:多态对已存在的代码具有可替换性
- 可扩充性:多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性,继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。
- 灵活性:在多态的应用中,体现了灵活多样的操作,提高了使用效率。
- 简化性:多态简化了应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出
多态的使用大多体现在实际开发中,多写代码,多用多态,慢慢自然能够体验到多态的灵活性以及多态的重要性
🍖4、三大特性总结
面向对象的三大特性:封装、继承、多态。
- 封装就是声明类的成员变量为私有的,同时提供公有的方法实现对该成员变量的存取操作。
- 继承是软件可重用性的一种表现,新类可以在不增加自身代码的情况下,通过从现有的类中继承其属性和方法充实自身内容,这种现象或行为被称为继承。
- 多态是具有表现多种形态的能力的特征。在程序设计的术语中,它意味着一个特定类型的变量可以引用不同类型的对象,并且能自动地调用引用地对象的方法,也就是根据作用到的不同的对象类型,响应不同的操作。
封装、继承、多态三大特性在Java中非常非常非常重要,一定要统统理解
🍖5、抽象类和接口
🍟5.1、抽象类
顾名思义,抽象类就是抽象出来的类,一般来说,具体类有直接对应的对象,而抽象类没有,它往往表达的是抽象的概念。
例如:
一只猫是具体对象,而猫科动物则是抽象的概念;玉米是具体对象,而作物则是抽象概念。
语法:
< 访问修饰符 > abstract class < 类名 >{ }
abstract 关键字表示该类被定义为抽象类
区别:
抽象类与普通类的最大区别就是:普通类可以被实例化,而抽象类不能被实例化
🍟5.2、抽象方法
当一个类的方法被 abstract 关键字修饰时,该方法被称为抽象方法。
注意:
抽象方法所在的类必须是抽象类,抽象类中可以包含抽象方法,也可以包含普通方法,还可以包含普通类包含的一切成员。
抽象方法:
一个方法被定义为抽象方法,意味着该方法不会有具体的实现(没有方法体),而在抽象类的子类中通过方法重写实现。
语法:
[ 访问修饰符 ] abstract <返回类型> < 方法名 > ( [ 参数列表 ] )
abstract 关键字表示该方法被定义为抽象方法
区别:
抽象方法与普通方法最大的区别是:普通方法有方法体,而抽象方法没有方法体。
public void print(){ } // 普通方法
public abstract void print(); // 抽象方法
总结:
1、在抽象类中,可以没有、有一个或多个抽象方法,甚至可以定义全部方法都是抽象方法。
2、抽象方法只有方法声明,没有方法实现。有抽象方法的类必须声明为抽象类。子类必须重写所有的抽象方法才能实例化;否则子类也必须声明成抽象类
3、抽象类可以有构造方法,其构造方法可以被本类的其他构造方法调用。若此构造方法不是由 private 修饰的,也可以被本类的子类中的构造方法调用。
🍟5.3、final 修饰符
final 是 java 关键字,表示 ”最后的、最终的、不可变的 “。
1、final 修饰的类:
用 final 修饰的类不能再被继承,没有子类
public final class Dog{
}
class Puppy extends Dog{ // 不能被继承
}
2、final 修饰类的方法
用 final 修饰的类的方法不能被子类重写
public class Student{
public final void print(){ // 不能被子类重写
System.out.println("记得快乐!");
}
}
3、final 修饰的变量
final 修饰的变量叫做常量,如定义成员变量,一定需要初始化,不然报错,如下:
public static final int i = 10;
final 修饰的局部变量只能赋值一次,不然报错
final int i;
i = 10;
i = 11; // 会报错,只能赋值一次
🍟5.3、final 与 abstract 修饰符
abstract:
可以用来修饰类和方法,不能用来修饰属性和构造方法。
final:
final 可以用来修饰类,方法和属性,不能修饰构造方法
🍟5.4、接口
接口类似一套规范,定义统一的标准,可以约束类的行为,使实现接口的类(或结构)在形式上保持一致
🍕5.4.1、定义和实现接口
接口定义:
接口类型的定义语法如下:
[访问修饰符] interface 接口名 {
// 接口成员
}
接口实现:
类实现接口的语法如下:
class 类名 implements 接口名{
// 类成员
}
接口中的属性都会自动加上 public static final 修饰,无论是否写了此关键字,接口的属性都是全局静态常量,必须在定义时指定初始值。
int P = 5; // ①
public static final int P = 5; // 效果与 ① 的效果一致
int P; // 没有初始化会报错
接口方法:
在接口中可以定义 抽象方法、默认方法、静态方法,如下:
public interface MyInterface {
// 抽象方法
void function1(); // 抽象方法没有方法体,非抽象子类必须重写
// 默认方法
default void function2(){
System.out.println("这是一个默认方法");
}
// 静态方法
static void function3(){
System.out.println("这是一个静态方法");
}
}
如下实现类:
public class MyClass implements MyInterface{
@Override
public void function1() { // 重写抽象方法
System.out.println("实现 MyInterface 接口的 function1()!");
}
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.function1(); //调用实现类中重写的方法
myClass.function2(); // 调用接口中的默认方法
MyInterface.function3(); // 调用接口中的静态方法
}
}
效果如下:
接口本身继承接口:
接口本身也可以继承接口,如下:
[ 访问修饰符 ] interface 接口名 extends 父接口1、父接口2、…{
// 常量定义
// 方法定义
}
实现多个接口:
一个普通类只能继承一个父类,但能同时实现多个接口,也可以同时继承一个父类并实现多个接口。如下:
class 类名 extends 父类名 implements 接口1、接口2…{
// 类的成员
}
注意:
普通类不管同时实现多少接口,必须把接口中的所有抽象方法全部重写。
🍟5.5、面向对象设计原则
在实际开发过程中,遵循以下原则会让代码更具有灵活性,更能适应变化。
1、摘取代码中变化的部分,形成接口
2、多用组合,少用继承
3、面向接口编程,不依赖于具体实现
4、针对扩展开放,针对改变关闭(开闭原则)
–5、面向接口编程,不依赖于具体实现–
总结:
* 抽象类使用 abstract 修饰,不能实例化
* 抽象类中可以用零个到多个抽象方法。抽象方法使用 abstract 关键字修饰,没有方法体。
* 如果非抽象类继承抽象类,则必须实现(重写)父类的所有抽象方法,否则子类只能是一个抽象类
* 用 final 关键字修饰的类,不能再被继承。用 final 修饰的方法,不能被子类重写。用 final 关键字修饰的变量将变成常量,只能在初始化时进行赋值,不能在其他地方修改。
* 接口中的属性都是全局静态常量。自 JDK 1.8 起,在接口中可以定义的方法包括抽象方法,静态方法和默认方法
* 类只能继承一个父类,但是可以实现多个接口。java 通过实现接口可以达到多重继承的效果。
* 接口表示一种约定,也表示一种能力。接口体现了约定和实现相分离的原则。通过面向接口编程,可以降低代码间的耦合性,提高代码的可扩展性和可维护性。
🍖6、异常
异常:
程序在运行过程中发生由于外部问题(如硬件错误、输入错误)等导致的程序异常事件。
(在Java等面向对象的编程语言中)异常本身是一个对象,产生异常就是产生了一个异常对象。
🍟6.1、异常处理机制
在程序设计时,必须考虑到可能发生的异常事件并进行相应的处理,这样才能保证程序正常运行。
🍟6.2、异常处理结构
java 针对异常的处理提供了 try、catch、finally、throws、throw 五个核心关键字,其中前三个关键字可以组成常用的异常处理结构。
语法:
try{
// 有可能出现异常的语句
}catch(异常类型 异常对象){
// 异常处理语句
}finally{
// 一定会运行到的语句
}
try 语句块用于监听,把有可能会出现异常的代码放在里面,当 try 中代码块中出现异常则会被 catch 捕捉,跳到 catch 语句块里面处理异常,finally 语句块总会被执行,finally 语句块常用于回收 try 语句块内打开的资源,如数据库连接,网络连接和磁盘文件。
常见异常处理格式:
异常格式的常见组合有 try-catch、try-catch-finally、try-finally 三种。
终止 finally 执行:
return 无法终止 finally 语句块执行,唯一能终止 finally 语句块执行的是 System.exit( 0 ) 表示停止 JVM 的执行
Exception 类型的常用方法
方法 | 描述 |
---|---|
void printStackTrace() | 输出异常堆栈信息。 |
String getMessage() | 返回异常的详细信息。该信息描述异常产生的原因,是 printStackTrace() 方法输出信息的一部分 |
🍕6.2.1、多重 catch 语句
语法:
try{
// 有可能出现异常的语句
}catch(异常类型 异常对象){
// 异常处理语句
}catch(异常类型 异常对象){
// 异常处理语句
}finally{
// 一定会运行到的语句
}
- 当 try 语句块中发生异常时,系统将会按照从上到下的顺序依次检测每个 catch 语句,当匹配到某条 catch 语句后,后续其他 catch 语句块将不会被执行
- 以 Exception 作为参数的 catch 语句必须放在最后的位置,否则所有的异常都会被其捕获,因为 Exception 是所有能处理异常的子类的父类
🍟6.3、异常继承结构图
异常的继承结构图如下:
1、Error 类:
java.lang.Error 类包含仅靠程序本身无法恢复的严重错误,一般指与 JVM 相关的问题,是 java 运行环境的内部错误或硬件问题,如内存资源不足,JVM 错误等。
2、Exception 类:
java.lang.Exception 类是程序本身可以处理的异常,可分为运行时异常(Run Time Exception )与 编译时异常( Checked )
2.1、运行时异常(Run Time Exception):
可以在程序中避免的异常。这类异常在编译代码时不会被编译器检测出来,可以正常编译运行,担当程序进行时发生异常,会输出异常堆栈信息并终止程序运行,如 空指针异常、类型转换异常、数组越界异常,这些异常包括 java.long.RuntimeException 类及其子类,可以使用 try-catch 语句捕获这类异常
java 程序中常见的运行时异常:
异常 | 说明 |
---|---|
ArithmeticeException | 出现算术错误时,抛出此异常 |
ArrayIndexOutOfBoundsException | 当非法索引访问数组时,抛出此异常 |
ClassCastException | 当试图将对象强制转换为非本对象类型的子类时,抛出此异常 |
IllegalArgumentException | 当向方法传递了一个不合法或不正确的参数时,抛出此异常 |
InputMismatchException | 当欲得到的数据类型与实际输入的类型不匹配时,抛出此异常 |
NullPointerException | 当应用程序试图在需要对象的地方使用 null 时,抛出此异常 |
NumberFormatException | 当试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出此异常 |
2.2、编译时异常(Checked):
Checked 异常:除运行时异常外的异常,是由用户错误或问题引起的异常,这是程序员无法预见的,常见的 编译异常有 FileNotFoundException 异常、SQLException 异常等
🍟6.4、声明异常——throws 关键字
throws 关键字是方法可能抛出异常的声明,使用 throws 关键字在方法上声明异常,声明异常之后,异常由上一级(调用者)处理
语法:
public void 方法名() throws 异常类型[,异常类型]{ // 声明的异常类型可多个
方法体
}
不建议在 main() 方法继续声明异常
public static void main(String[] args) throws Exception{
// 方法体
}
main( ) 方法中声明异常由 JVM 处理,如果程序出现了错误,会导致程序中断执行。
🍟6.5、抛出异常——throw 关键字
可以使用 throw 关键字手动抛出异常,使用 new 关键字创建异常类的对象,通过 throw 关键字抛出。
语法:
throw new 异常名( [ 参数列表 ] );
// 例如:
throw new Exception();
注意:
使用 throw 关键字抛出的只能是 Throwable 类或其子类的对象
🍟6.6、自定义异常
当 java 异常体系中提供的异常类型不能满足程序的需要时,可以自定义异常类,使用自定义异常一般有如下三个步骤:
(1)定义 异常类,继承 Exception 类或 RuntimeException 类。
(2)编写异常类的构造方法,并继承父类的实现。
(3)实例化自定义异常对象,并使用 throw 关键字抛出。
如下自定义异常:
/**
* 年龄异常
*/
public class AgeException extends Exception{
public AgeException(){
super();
}
public AgeException(String message){
super(message);
}
public AgeException(String message, Throwable cause) {
super(message, cause);
}
}
上面自定义异常的 Test 类
public class ThrowTest {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请录入您的年龄:");
int age = input.nextInt();
try{
ThrowTest test = new ThrowTest();
System.out.println(test.ShowTicketPrice(age));
}catch (Exception ex){
System.out.println( ex.getMessage());
}
}
/**
* 根据年龄显示票价信息
* @param age
* @return
* @throws Exception
*/
private String ShowTicketPrice(int age) throws Exception{
if(age < 3){
throw new Exception("您录入的年龄有误,3岁以下儿童暂不能观影!"); // 抛出自定义异常
}else if(age >= 60 || age <= 6){
return "您可以购买半价票 25 元!";
}else{
return "您需要购买全票价 50 元!";
}
}
}
🍟6.7、异常总结
1、异常是由 java 应用程序抛出和处理的非严重错误,它可以分为运行时异常和 Checked 异常两大类。
2、Checked 异常必须捕获或声明抛出,否则无法通过编译。运行时异常不要求必须捕获或声明抛出。
3、java 异常处理是通过五个关键字实现的:try、catch、finally、throw、throws。功能如下:
* try:用户监听。将可能抛出异常的代码放在 try 语句块之内,当 try 语句块内发生异常时,会跳到 catch 语句块内执行。
* catch:用户捕获异常。catch 语句用来捕获 try 语句块中发生的异常。
* finally:finally 语句块总是会被执行。它主要用于回收在 try 语句块里打开的资源(如数据库连接)
* throw:用于抛出异常
* throws:用于声明方法中可能抛出的异常。在该方法中可以不强制进行异常处理,如果出现了异常,则交给(上层)调用者进行处理。
4、可以在一个 try 块后跟随多个 catch 块,分别处理不同的异常,但排列顺序必须是从特殊到一般,最后一个一般为 Exception 类。
🍖7、集合框架
Java 集合位于 java.util 包中,集合中的元素全部是对象,及 Object 实例,不同的集合类有不同的功能和特点,适合不同的场合。
🍟7.1、集合结构
集合的继承结构:
ps:上图源自于网络
Java 的集合类主要由两个接口派生而成:Collection 接口和 Map 接口。Collection 接口和 Map 接口是 Java 集合框架的根接口,其中,Collection 接口常用的子接口包含 List 接口和 Set 接口;
Map 接口是独立的
Java 集合框架常用接口:
名称 | 描述 |
---|---|
Collection | * 单值集合的根接口,是最基本的集合接口。 * 一个 Collection 代表一组 Object。Java 不提供实现类,只提供子接口(如 List 接口和 Set 接口)。 * Collection 接口存储一组可重复的无序对象。 |
Set | * 继承 Collection 接口,存储一组不可重复的无序对象(无序不重复)。 |
List | * 继承 Collection 接口,储存一组可重复的有序对象。 * 元素顺序以元素插入的次序来放置元素,不会重新排序。 * 通过索引访问数组元素,索引从0开始。 * List 接口常用的实现类有 ArrayList 类和 LinkedList 类。 |
Map | * 存储成对数据的根接口,可存储一组 “键-值对” 对象。 * 提供键(key)到值(value)的映射,Map 中的键不要求有序,不允许重复。值同样不要求有序,但允许重复。 * 可以通过键找到对应的值。 |
Iterator | 集合迭代器,能够遍历集合元素的接口 |
Collections | * 与 Collection 是不同的概念,它提供了对集合对象进行基本操作的通用接口方法。 * 包含各种有关集合操作的静态方法。 * 一个工具类,不能实例化。 |
🍕7.1.2、Collection 接口
Collection 接口是 List 接口、Set 接口的父接口,该接口中定义的方法即可用于操作 Set 集合,也可用户操作 List 集合。
Collection 接口常用方法:
ps:详细自行查找 API
注意:
Java 集合中的元素全部是对象,是引用类型,不能存储基本数据类型元素。如果强制添加,则会自动封装成对象(自动装箱,自动拆箱)。
🍟7.2、List
List 集合是一类存储元素有序,可重复的集合,集合中每个元素都有其对应的索引。List 集合允许使用重复元素,通过索引访问元素。
🍕7.2.1、List 概述
List 接口作为 Collection 接口的子接口,可以使用 Collection 接口定义的全部方法。
List 接口在 Collection 接口方法的基础上,另外扩展了一些根据索引操作集合元素的方法。
List 接口扩展的方法:
🍕7.2.2、ArrayList 集合类
继承 List 接口,底层数组,可通过索引访问元素
ArrayList 集合的基本使用如下:
import java.util.ArrayList;
import java.util.Iterator;
/*
1.1、每个集合对象的创建(new)
1.2、向集合中添加元素
1.3、从集合中取出某个元素
1.4、遍历集合
*/
public class ArrayListTest {
public static void main(String[] args) {
// 创建ArrayList集合
ArrayList<String> arrayList = new ArrayList<>();
// 向集合添加元素
arrayList.add("zhangsan");
arrayList.add("lisi");
arrayList.add("wangwu");
arrayList.add("zhaoliu");
// 从集合中取出某个元素
String i = arrayList.get(1);
System.out.println(i);
System.out.println("===============分割线============");
// 遍历集合 获取迭代器进行集合遍历
Iterator<String> it = arrayList.iterator();
while(it.hasNext()){
System.out.print(it.next()+" ");
}
System.out.println("\n===============分割线============");
// 遍历集合 通过迭代器配合for循环遍历
for(Iterator<String> its = arrayList.iterator() ;its.hasNext();){
System.out.println("====>"+its.next());
}
System.out.println("===============分割线=============");
// 遍历集合 通过增强for
for(String fors : arrayList){
System.out.print(fors+" ");
}
System.out.println("\n===============分割线=============");
// 遍历集合 通过获取长度加for的方式
for(int j = 0;j < arrayList.size();j++){
System.out.print(arrayList.get(j)+" ");
}
}
}
运行结果:
总结:
ArrayList 集合底层采用数组的数据结构
ArrayList 集合是非线程安全的
1、ArrayList 集合初始化容量是 10
2、ArrayList 集合底层是 Object 类型的数组 Object[ ]
3、扩容到原容量的 1.5 倍
4、建议给定一个预估计的初始化容量,减少数组的扩容次数,这是 ArrayList 集合比较重要的优化策略。
5、数组的优点:检索效率比较高
6、数组的缺点:随机增删元素效率比较低
7、需要注意的是:向数组末尾添加元素,效率还是很高的
🍕7.2.3、LinkedList 集合类
- ArrayList集合 采用了和数组相同的存储方式,在内存中分配连续的空间,当添加和删除非尾部元素时会导致后面所有元素的移动,性能低下,所以在插入,删除操作比较频繁时,可以考虑使用LinkedList 集合提高效率。
- LinkedList 类是 List 接口的实现类,这就意味着 LinkedList 集合和 ArrayList 集合都是一个 List 集合,可以根据所以随机访问集合中的元素。
- LinkedList 集合具有 双向链表结构,更加方便实现添加和删除操作。
LinkedList 集合方法如下:
LinkedList 集合存储数据与遍历:
import java.util.LinkedList;
import java.util.List;
/**
* 使用 LinkedList 集合存储数据
*/
public class LinkedListTest {
public static void main(String[] args) {
// 1.创建集合及元素对象
LinkedList fruits = new LinkedList();
Fruit fruit1 = new Fruit("香水梨",2.5);
Fruit fruit2 = new Fruit("苹果梨",2.0);
Fruit fruit3 = new Fruit("富士苹果",3.5);
Fruit fruit4 = new Fruit("金帅苹果",3.0);
//2.向集合中添加元素
fruits.add(fruit1);
fruits.add(fruit2);
fruits.add(fruit3);
fruits.add(fruit4);
System.out.println("集合中包含水果信息如下:");
shoData(fruits);
// 3.查看集合中第一条水果信息
Fruit firstFruit = (Fruit) ((LinkedList) fruits).getFirst();
System.out.println("集合中第一类水果:"+firstFruit.getBrand());
// 5.删除集合中第一条和最后一条信息
fruits.removeFirst();
fruits.removeLast();
System.out.println("删除部分信息后还有"+fruits.size()+"条水果信息。如下:");
shoData(fruits);
}
/**
* for 循环遍历集合
* @param list
*/
private static void shoData(List list) {
for (int i = 0; i < list.size(); i++) {
Fruit fruit = (Fruit) list.get(i);
fruit.show();
}
}
}
水果类基本结构:
/**
* 水果类
*/
public class Fruit {
private String brand; // 水果品种
private double price; // 价格
// 省略 setter and getter
public Fruit() {
}
public Fruit(String brand, double price) {
this.brand = brand;
this.price = price;
}
// 输出信息
public void show(){
System.out.println(this.brand+",每斤"+this.price+"元。");
}
}
运行结果:
总结:
LinkedList 集合底层采用双向链表数据结构
1、LinkedList 集合是双向链表。
2、对于链表数据结构来说,随机增删效率较高。检索效率较低。
3、链表中的元素在空间存储上,内存地址不连续。
ArrayList 集合与 LinkedList 集合:
ArrayList 集合底层是数组。
- 优点:基于数组实现,读取操作效率高。
- 缺点:不适合频繁进行插入和删除操作,因为每次执行该类操作都需要频繁移动其中的元素。
LinkedList 集合由双向链表实现,从任意一个节点开始,都可以很方便地访问它地前驱节点和后继节点。(双向链表的头节点和尾节点)
- 优点:增加、删除操作只需修改链表节点指针,无需进行频繁的移动。
- 缺点:遍历效率较低。
🍟7.3、Set
Set 接口和 List 接口一样,都是 Collection 的子接口,Set 集合中不允许包含重复元素,没有顺序。
🍕7.3.1、HashSet 集合
HashSet 类是 Set 接口的典型实现,使用 HashSet 集合可以实现对无序不重复数据的存储,具有很好的存取好查找性能。
特征:
- 不允许存储重复的元素。
- 没有索引,没有包含索引的方法,不能使用索引遍历。
- 是无序集合,存储元素和取出元素的顺序可能不一致。
如下案例:
import java.util.HashSet;
import java.util.Set;
/**
* 使用 HashSet 集合存储数据
*/
public class HashSetTest {
public static void main(String[] args) {
// 1.创建一个 Set 集合和多条水果数据
Set fruits = new HashSet();
Fruit fruit1 = new Fruit("金帅苹果",2.5);
Fruit fruit2 = new Fruit("富士苹果",2.0);
// 2.向集合中添加元素
fruits.add(fruit1);
fruits.add(fruit2);
System.out.println("添加重复元素,是否成功::"+fruits.add(fruit1));
// 3.遍历集合
for (Object o : fruits) {
Fruit fruit = (Fruit) o;
fruit.show();
}
}
}
运行结果:
总结:
通过源码发现,实际上 HashSet 集合在 new 的时候,底层事件上 new 了一个 HashMap 集合,向 HashSet 集合中存储元素,实际上是存储到 HashMap 集合中。
HashMap 集合是一个哈希表数据结构
HashSet 集合初始化容量是 16 初始化容量建议是 2 的倍数。
扩容:扩容之后是原容量2倍
🍕7.3.2、TreeSet 集合类
TreeMap 集合的创建、添加、取出、遍历
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
/*
1.1、每个集合对象的创建(new)
1.2、向集合中添加元素
1.3、从集合中取出某个元素
1.4、遍历集合
1.5、测试TreeSet集合中的元素是可排序的。
1.6、测试TreeSet集合中存储的类型是自定义的。
1.7、测试实现Comparable接口的方式
1.8、测试实现Comparator接口的方式(最好测试以下匿名内部类的方式)
*/
public class TreeSetTest {
public static void main(String[] args) {
// 创建 TreeSet 集合
TreeSet<String> treeSet = new TreeSet<>();
// 向集合中添加元素
treeSet.add("zhangsan");
treeSet.add("lisi");
treeSet.add("wangwu");
treeSet.add("zhaoliu");
// 从集合中取出元素 转为 ArrayList 集合通过下标取出
ArrayList arr = new ArrayList(treeSet);
System.out.println(arr.get(0));
System.out.println("===============分割线============");
// 集合遍历 获取迭代器遍历
Iterator<String> it = treeSet.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
System.out.println("===============分割线============");
// 遍历集合 通过增强for
for (String s : treeSet) {
System.out.println(s);
}
System.out.println("===============分割线============");
// 测试TreeSet集合中的元素是可排序的
TreeSet<Integer> treeSet1 = new TreeSet<>(); // 结果 1 2 3 4
treeSet1.add(2);
treeSet1.add(4);
treeSet1.add(1);
treeSet1.add(3);
for (Integer integer : treeSet1) {
System.out.println(integer);
}
System.out.println("===============分割线============");
//测试TreeSet集合中存储的类型是自定义的
/* TreeSet<A> treeSet2 = new TreeSet<>();
treeSet2.add(new A(1));
treeSet2.add(new A(3));
treeSet2.add(new A(2));
treeSet2.add(new A(4));
//遍历集合
for (A a : treeSet2) {
System.out.println(a);
}*/
/* 会报
Exception in thread "main"
java.lang.ClassCastException: Gather.GatherZoJie.A cannot be cast to java.lang.Comparable
这个异常,原因是类未实现Comparable接口
*/
System.out.println("===============分割线============");
// 测试实现Comparable接口的方式
TreeSet<B> treeSet3 = new TreeSet<>();
treeSet3.add(new B(1));
treeSet3.add(new B(3));
treeSet3.add(new B(2));
treeSet3.add(new B(4));
// B 类以实现 Comparable 接口,同时重写了 toString 方法
// 遍历
for (B b : treeSet3) {
System.out.println(b);
}
// 遍历结果是按 重写的compareTo方法的排序方式进行排序
System.out.println("===============分割线============");
//测试实现Comparator接口的方式(最好测试以下匿名内部类的方式)
// 比较器方法
TreeSet<C> treeSet4 = new TreeSet<>(new Comparator<C>() {
@Override
public int compare(C o1, C o2) {
return o1.age - o2.age;
}
});
treeSet4.add(new C(1));
treeSet4.add(new C(3));
treeSet4.add(new C(2));
treeSet4.add(new C(4));
// 遍历
for (C c : treeSet4) {
System.out.println(c);
}
// 输出结果 1 2 3 4 以排序
}
}
// 自定义类型A
class A{
int age;
public A(int age){
this.age = age;
}
}
// 自定义类型B,实现 Comparable 接口,重写 compareTo 排序方法
class B implements Comparable<B>{
int age;
public B(int age){
this.age = age;
}
@Override
public int compareTo(B o) {
return this.age - o.age;
}
@Override
public String toString() {
return "B{" +
"age=" + age +
'}';
}
}
// 自定义类型A
class C{
int age;
public C(int age){
this.age = age;
}
@Override
public String toString() {
return "B{" +
"age=" + age +
'}';
}
}
总结:
TreeSet 集合底层实际上是 TreeMap
new TreeSet 集合的时候,底层实际上new了一个TreeMap 集合。
往 TreeSet 集合中放数据的时候,实际上是将数据放到 TreeMap 集合中,
TreeMap 集合底层采用二叉树数据结构,能按照大小顺序排序。
注意:
1、HashSet 底层 HashMap
2、TreeSet 底层 TreeMap
🍟7.4、Map
List 集合和 Set 集合都可以存储一组数据元素,每个元素是一个对象,并且可以实现对这组数据元素的操作。
特点:
1、Map 集合和 Collection 集合没有关系。
2、Map 集合以 key 和 value 的这种键值对的方式存储元素。
3、key 和 value 都是存储 java 对象的内存地址。
4、所有 Map 集合的 key 特点:无序不可重复的。
Map 集合的 key 和 Set 集合存储元素特点相同。
Map接口方法:
Map 接口提供了大量实现类,典型实现类如 HashMap 类、HashTable 类、TreeMap 类等。
🍕7.4.1、HashMap 集合类
HashMap 集合的创建、添加、取出、遍历
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/*
1.1、每个集合对象的创建(new)
1.2、向集合中添加元素
1.3、从集合中取出某个元素
1.4、遍历集合
*/
public class HashMapTest {
public static void main(String[] args) {
// 创建 HashMap 集合
HashMap<Integer,String> hashMap = new HashMap<>();
// 向集合中添加元素
hashMap.put(1,"zhangsan");
hashMap.put(2,"lisi");
hashMap.put(3,"wangwu");
hashMap.put(4,"zhaoliu");
// 从集合中取出某个元素
String s = hashMap.get(1);
System.out.println(s);
// 遍历集合 获取所有的key,然后通过key获取value
Set<Integer> it = hashMap.keySet();
for (Integer integer : it) {
System.out.println(integer + " =" + hashMap.get(integer));
}
System.out.println("===============分割线============");
// 遍历集合 通过 调用 entrySet方法 得到一个 Set
Set<Map.Entry<Integer, String>> entries = hashMap.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println(entry.getKey() + " =" + entry.getValue());
}
}
}
总结:
HashMap 集合底层是哈希表数据结构
HashMap 集合是非线程安全的
在 JDK8 之后,如果哈希表单向链表中元素超过 8 个,单向链表这种数据结构会变成红黑树数据结构。当红黑树上的节点数量小于 6 时,会重新把红黑树变成单向链表数据结构。
这种方式也是为了提高检索效率,二叉树的检索会再次缩小扫描范围。提高效率。
初始化容量 .16,默认加载因子.75
扩容:扩容之后的容量是原容量的 2 倍。
HashMap 集合的 key 和 value 允许 null
🍟7.5、集合总结
1、集合弥补了数组的缺陷,它比数组更灵活更实用,可大大提高软件的开发效率,而且不同的集合可用于不同场合。
2、Java 的集合类主要由两个接口派生而出:Collection 接口和 Map 接口及相关的工具类,这两个接口又包括了一些子接口或实现类。其中,Collection 接口又包含了两个子接口—— List 接口和 Set 接口,另外一个重要接口是 Map 接口。
* Collection 接口存储一组不唯一,无序的对象。
* Set 接口继承 Collection 接口,存储一组唯一,无序的对象。
* List 接口继承 Collection 接口,存储一组不唯一,有序的对象
* Map 接口存储一组成对的 键-值对象,提供从 key 到 value 的映射。key 不要求有序,不允许重复;value 同样不要求有序,但允许重复。
3、Iterator 为集合而生,专门实现集合的遍历。它隐藏了各种集合实现类的内部细节,提供了遍历集合的统一编程接口。
4、ArrayList 类和数组采用相同的存储方式,它的优点在于遍历元素和随机访问元素的效率比较高。
5、LinkedList 类采用链表存储方式,优点在于插入,删除元素时效率比较高。
6、HashMap 类是最常见的 Map 实现类,它的存储方式是哈希表,优点是查询指定元素效率高。
🍖8、包装类
Java 包含八种基本数据类型,同时包含与其对应的包装类。
这些包装类存在于 java.lang 包中,关系图如下:
其中:
所有数字类型包装类都继承 Number 类,Number 类是一个抽象类。
Number 类包装了 Byte、Short、Interger、Long、Float、Double 等数字类型,并且实现其所定义的方法,这些方法以不同的数字格式返回对象的值。
🍟8.1、包装类和基本数据类型的转换
🍕8.1.1、基本数据类型转换为包装类
在 Java 中,基于基本数据类型数据创建包装类对象通常可用采用如下两种方式:
public Type(type value)
public Type(String value)
其中,Type 表示包装类,参数 type 为基本数据类型。
如下:
/**
* 基本数据类型向包装类转换
*/
public class Test{
public static void main(String[] args) {
boolean bl = true;
// 1.通过构造方法现基本数据类型向包装类转换
Boolean blObj = new Boolean(bl);
Integer itOBj = new Integer(35);
Character chOBJ = new Character('男');
System.out.println(blObj+","+itOBj+","+chOBJ);
// 2.将字符串转换为 Boolean 对象
Boolean bOBj = new Boolean("true");
Boolean bOBj2 = new Boolean("TRue"); // 不区分大小写
Boolean bOBj3 = new Boolean("true"); // 非 true 即 false
System.out.println(bOBj+","+bOBj2+","+bOBj3);
// 运行时将出现 java.lang.NumberFormatException 异常
// Long lObj = new Long("hello");
// Char 型数据不能使用字符串构造包装类对象
// Character chobj2 = new Character('男');;
}
}
运行结果:
true,35,男
true,true,true
🍕8.1.2、包装类转换为基本数据类型
包装类转换为基本数据类型通常采用如下方法:
public type typeValue();
其中:type 指的是基本数据类型,如 byteValue( )、charValue( )、相应的返回值为 byte、char。将包装类对象转换为基本数据类型。
如下:
/**
* 包装类转换为基本数据类型
*/
public class Test {
public static void main(String[] args) {
boolean bl = true;
// 通过构造器实现基本数据类型向包装类转换
Boolean blObj = new Boolean(bl);
Integer itObj = new Integer(35);
Character chObj = new Character('男');
// 包装类转换为基本数据类型
boolean b1 = blObj.booleanValue();
int i = itObj.intValue();
char ch = chObj.charValue();
System.out.println(b1+","+i+","+ch);
}
}
运行结果:
true,35,男
关系图:
🍟8.2、装箱拆箱
Java 基本数据类型变量和包装类对象之间的转换较繁琐,从 JDK1.5 之后提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)机制
- 自动装箱:把基本数据类型变量直接转换为对应的包装类对象,或者转换为 Object 对象。
- 自动拆箱:与装箱相反,将包装类对象转换成对应的基本数据类型变量
如下:
public class BoxTest {
public static void main(String[] args) {
// 基本数据类型变量转换为包装类(装箱)
Integer inObj = 5;
Object boolObj = true;
System.out.println(inObj+","+boolObj);
// 包装类转换为基本数据类型(拆箱)
int it = inObj;
System.out.println(it);
if(boolObj instanceof Boolean){
// 先把 Object 对象强制转换为 Boolean 类型,再赋值给 boolean 变量
boolean b = (Boolean)boolObj;
System.out.println(b);
}
}
}
运行结果:
5,true
5
true
🍖9、I / O 流
🍟9.1、File 类
File 类是 java.io 包下代表操作与平台无关的文件和目录的类。可以通过 File 类实现对文件或目录的新建、删除、重命名等操作。
File 类的构造方法定义如下:
public File(String pathName)
File 类的方法如下:
🍟9.2、I / O 流概述
I / O 流就是可以操作文件的一些 Java类,它们在 java.io 包下
I 即 input,指读取操作;O 即 output,指写出操作
🍕9.2.1、按流向划分:输入流和输入流
输入流:只能从中读取数据,而不能写入数据的流,实现程序从数据源中读操作。(从硬盘输入到内存)
输出流:只能向其写入数据,而不能从中读数据的流,实现程序向目标数据源中写数据。(从内存到硬盘)
Java 的输入流主要以 inputStream 和 Reader 作为基类,而输出流则主要以 OutputStream 和 Writer 作为基类。它们都是一些抽象类,无法直接实例化对象。
🍕9.2.2、按处理单元划分:字节流和字符流
字节流:以 8 位字节位为操作数据单元的流,可用于操作二进制数据。
字符流:以 16 位的字符为操作数据单元的流,可用于操作文本数据。
理解:
可以把 I / O 流看作一个水管,这个水管中依次排列着许多水滴,每滴水滴就是一个处理单元,即一个字节或字符。在字节流中每滴水滴是一个字节,在字符流中每滴水滴是一个字符。
🍕9.2.3、按流的角色划分:节点流和处理流
节点流:可以直接向一个特定的存储介质(如磁盘、文件)读写数据的流。
处理流:用于对一个已存在的流进行连接和封装,是通过封装后的流实现数据读写操作的流。
画图如下:
ps:以上图片源于网络
🍟9.3、字节流
java.io 包中的流按存储单元分类主要又字节流和字符流两大类,两类都具有输入/输出操作。它们的基类如下图:
以上基类均为抽象类
🍕9.3.1、字节输出流基类:OutputSteam
OutputStream 类为抽象类,必须使用该类的子类进行实例化对象。
OutputStream 类的方法如下:
🍕9.3.2、字节输出流 FileOutputStream 类
FileOutputStream 类是 OutputSteam 类的子类
FileOutputStream 构造方法:
实现实例如下:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* 字符输出流(写)测试
*/
public class OutputStreamTest {
public static void main(String[] args) {
OutputStream fos = null;
try {
// 2.创建文件输出流对象
fos = new FileOutputStream("test2.txt"); // 里面是文件路径
// 3.执行写操作
String str = "I love Java"; //准备一个字符串
byte[] words = str.getBytes(); //将字符串转换为 byte 数组
fos.write(words,0,words.length);
System.out.println("文件写入成功!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
// 4.关闭输出流
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
🍕9.3.3、字节输入流基类:InputStream
InputStream 类为抽象类,必须使用该类的子类进行实例化对象。
InputStream 类的方法如下:
🍕9.3.4、字节输入流 FileInputStream 类
通常使用 InputStream 类的 FileInputStream 子类实现文本文件内容的读取
FileInputStream 类构造方法如下:
实现实例如下:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
* 字节输入流(读)
*/
public class InputStreamTest {
public static void main(String[] args) {
InputStream fis = null;
try {
// 2.创建文件输入流对象
fis = new FileInputStream("test2.txt"); // 里面是路径
System.out.println("可读取的字节数:"+fis.available());
// 3.执行读操作
int data = 0;
// 循环读取数据
System.out.println("文件内容:");
while((data = fis.read()) != -1){
System.out.print((char)data+" ");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
if (fis != null) {
try {
// 4.关闭输出流
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
可以使用数组一次性读取多个字节。如下:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
// 从文件中读数据
public class InputStreamTest {
public static void main(String[] args) {
InputStream fis = null;
try {
// 2.创建文件输入流对象
fis = new FileInputStream("test2.txt");
// 3.执行读操作
byte[] words = new byte[1024]; // 一次读取 1024 字节
int len = 0; //实际读取长度
System.out.println("文件的内容:");
while ((len = fis.read(words))!= -1) {
System.out.println(new String(words,0,len));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
🍟9.4、字符流
在 Java 中,一个字符占用内存的两个字节。如果用字节流处理文本文件,则程序内部需要将字节转换成字符,减低了执行效率,所以当输入和输出文本文件时,尽量使用字符流。Java 提供了 Writer 类和 Reader 类来操作字符。
🍕9.4.1、字符输出流基类:Writer
Writer 类是字符输出流的抽象类。
Writer 类的方法如下:
🍕9.4.2、字符输出流 FileWriter 类
FileWriter 类是 Writer 类常用的子类,使用该类可以以字符为数据处理单元向文本文件中写数据。
实现实例如下:
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
/**
* 字符输出流 FileWriter
*/
public class FileWriterTest {
public static void main(String[] args) {
Writer fw = null;
try{
//创建 FileWriter 对象
fw = new FileWriter("text.txt");
// 向文件写入数据
fw.write("预测未来的最好方\n式就是创造未来!");
fw.flush(); // 刷新缓冲区
System.out.println("文件写入成功!");
} catch (IOException e) {
e.printStackTrace();
System.out.println("文件不存在!");
}finally{ // 关闭流
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
🍕9.4.3、字符输入流基类:Reader
Reader 类是字符输入流的基类,它是抽象类。
Reader 类的方法如下:
🍕9.4.4、字符输入流 FileReader
Reader 类同样是抽象类,可以使用其子类 FileReader 创建对象。
实现实例如下:
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
/**
* 字符输入流
*/
public class FileReaderTest {
public static void main(String[] args) {
Reader fr = null;
try{
// 2.实例化 FileReader 对象
fr = new FileReader("text.txt");
char[] ch = new char[1024]; //字符使用 char 数组
int len = 0; // 保存实际存储的字符数
// 3.循环读取数据
while((len = fr.read(ch)) != -1){
System.out.println(new String(ch,0,len));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
// 4.关闭流
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
🍟9.5、缓冲流
java.io 包提供了缓冲流,缓冲流属于处理流。
🍕9.5.1、字符缓冲输出流 BufferedWriter 类
BufferedWriter 类是 Writer 类的子类,BufferedWriter 类可以把一批数据写到缓冲区,默认情况下,只有在缓冲区满的时候,才会把缓冲区的数据真正写到目的地,这样能减少物理写数据的次数,从而提高 I/O 操作的执行效率。
BufferedWriter 类的构造方法如下:
实现实例如下:
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
/**
* 字符缓冲输出流 BufferedWriter 类
*/
public class BufferedWriterTest {
public static void main(String[] args) {
Writer fw = null;
BufferedWriter bw = null;
try{
// 创建 FileWriter 对象
fw = new FileWriter("BufferedWriter.txt");
bw = new BufferedWriter(fw);
// 向文件中写数据
bw.write("亲爱的小伙伴们:");
bw.newLine(); //换行
bw.write("让我们来使用缓冲流让程序加速吧!");
bw.flush(); // 刷新
System.out.println("文件写入成功!");
} catch (IOException e) {
e.printStackTrace();
System.out.println("文件不存在!");
}finally {
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
从以上可以看出,创建 BufferedWriter 对象需要借助一个 Writer 对象,在该对象的基础上进一步封装,成为一个带缓冲区的字符输出流。
🍕9.5.2、字符缓冲输入流 BufferedReader 类
BufferedReader 类是 Reader 类的子类,它与 FileReader 类的区别在于 BufferedReader 类有缓冲区,它可以把一批数据读到缓冲区中,接下来的读操作都是从缓冲区内获取数据,避免每次都从数据源读取数据进行字符编码转换,从而提高读取操作的效率。
BufferedReader 类的构造方法如下;
实现实例如下:
import java.io.*;
/**
* 带缓冲区的字符输入流 BufferedReader 类
*/
public class BufferedReaderTest {
public static void main(String[] args) {
Reader fr = null;
BufferedReader br = null;
try{
fr = new FileReader("BufferedWriter.txt");
br = new BufferedReader(fr);
String line = br.readLine();
while(line != null){
System.out.println(line);
line = br.readLine();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (fr != null) {
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
从以上可以看出,创建 BufferedReader对象需要借助一个 Reader对象,在该对象的基础上进一步封装,成为一个带缓冲区的字符输入流。
🍟9.6、数据操作流
可以对二进制文件的读写操作
🍕9.6.1、DataOutputStream 类和 DataInputStream 类
DataOutputStream 类是 OutputStream 类的子类,利用 DataOutputStream 类写二进制文件的实现步骤与使用 FileOutputStream 类写文件的步骤及其相似,而且用到了 FileOutputStream 类。
同样:DataInputStream 类是 InputStream 类的子类,在使用上也与 FileInputStream 类很相似。
使用实例如下:
import java.io.*;
/**
* 数据操作流
*/
public class DataInOutTest {
public static void main(String[] args) throws IOException {
DataOutputStream dos = null;
DataInputStream dis = null;
FileInputStream fis = null;
FileOutputStream fos = null;
try{
// 创建 输入流文件
fis = new FileInputStream("D:\\User.class");
dis = new DataInputStream(fis);
// 创建输出流文件
fos = new FileOutputStream("Demo/newUser.class");
dos = new DataOutputStream(fos);
int temp;
while((temp = dis.read()) != -1){
fos.write(temp);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally { // 关闭
if (dis != null) {
dis.close();
}
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
if (dos != null) {
dos.close();
}
}
}
}
🍟9.7、序列化与反序列化
将对象的信息保存到磁盘中便于以后检索。可以将对象中的属性信息逐一记录到文本文件中(序列化),还需要再使用的时候还原(反序列化)。
序列化的对象保存的是二进制状态,这样实现了平台无关性,可以将在 Windows 操作系统中实现序列化的一个对象,传输到 Linux 操作系统的机器上。反序列化是将特定的存储介质中的数据重新构建对象的过程,通过发序列化后得到相同对象,而无序担心数据因平台不同出现异常。
序列化和反序列化过程:
实现序列化和反序列化操作,需要使用对象操作流:
对象输出流:ObjectOutputStream
对象输入流:ObjectInputStream
🍕9.7.1、对象输出流 ObjectOutputStream 实现序列化
可以理解为把一个对象变为二进制的数据流的一种方法,通过对象序列化可以方便的实现对象的传输和存储。
一个类需要被序列化,则这个对象所属类必须实现 java.io.Serializable 接口,接口如下:
public interface Serializable{}
序列化版本号:
对象序列化和反序列化操作时可能会遇到版本兼容性问题,要考虑 JDK 版本,版本不一致就可能发生错误。
所以在序列化操作中引入了一个 serialVersionUID 的常量(序列化版本号),可以通过此常量验证版本的一致性。
当实现 java.io.Serializable 接口的类没有显式定义名为 serialVersionUID、类型为 long 的变量时,java 序列化机制在编译时会自动生成一个此版本的 serialVersionUID 变量。如果不希望通过编译自动生成,可以直接自己显式定义这个变量。
private static final long serialVersionUID = 1L; (1L为自己定义的版本号)
ObjectOutputStream 类的常用方法:
方法名 | 描述 | 类型 |
---|---|---|
ObjectOutputStream(OutputStream out) | 创建对象输出流对象 | 构造方法 |
final void writeObject(Object obj) | 将指定对象写入流 | 实例方法 |
实现实例如下:
import java.io.*;
/**
* 序列化与反序列化
*/
public class SerializableObj {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try{
// 创建 ObjectOutputStream 输出流
oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
Person person = new Person("杰米",25,"男"); // Person 类实现了 Serializable 接口
// 对象序列化,写入数据
oos.writeObject(person);
System.out.println("序列化成功!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
也可以使用集合一次性保存多个对象
🍕9.7.2、对象输入流 ObjectInputStream 实现反序列化
ObjectInputStream 它可以直接把序列化后的对象还原。
ObjectInputStream 常用方法:
方法 | 描述 | 类型 |
---|---|---|
ObjectInputStream(InputStream in) | 创建对象输入流对象 | 构造方法 |
final Object readObject( ) | 从指定位置读取对象 | 实例方法 |
实现实例如下:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
/**
* 反序列化
*/
public class DeserTest {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("Demo/person.txt"));
// 反序列化,进行强制类型转换
Person per = (Person) ois.readObject();
// 输出转换反序列化后的对象信息
per.print();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally{
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注意:
如果使用序列化方式向文件中写入多个对象,那么反序列化恢复对象时,也按照写入的顺序读取。
🍕9.7.3、transient 关键字
如果希望对象中的某个属性不被序列化,只需在某个类中的属性前添加 transient 关键字,如下:
private transient int age;
age 属性将不会参与序列化
🍕9.7.4、Java 体系中常用流总结
Java 中常用的流总结如下:
分类 | 字节输出流 | 字节输入流 | 字符输出流 | 字符输入流 |
---|---|---|---|---|
基类 | OutputStream | InputStream | Writer | Reader |
文件流 | FileOutputStream | FileInputStream | FileWriter | FileReader |
缓冲流 | BufferedOutputStream | BufferedInputStream | BufferedWriter | BufferedReader |
对象流 | ObjectOutputStream | ObjectInputStream | —— | —— |
数据操作流 | DataOutputStream | DataInputStream | —— | —— |
所有的基类都是抽象类,无法直接创建实例,需要借助其实现类
所有的输出流用于实现写数据操作,所有的输入流用于实现读取操作。这个输入和输出是相对程序而言的。
所有的文件流直接与存储介质关联,也就是需指定物理节点,属于节点流,其他流的创建需要在节点流的基础上进行封装,使其具有特殊的功能。
在操作文本文件时,应使用字符流。字节流可以处理二进制数据,它的功能比字符流更强大。
因为计算机所有的数据都是二进制数据,如果用字节流处理文本文件,还需要把这些字节转换成字符,就增加了编程的复杂度,减低了执行效率。所以当输入和输出是文本文件时,尽量使用字符流。如果是二进制文本,则应考虑使用节点流。
🍖10、多 线 程
🍟10.1、了解多线程
世间万物每个个体都可以同时完成很多工作,这就是多线程
🍕10.1.1、进程与线程
进程是程序的一次动态执行过程,它是从代码加载、执行中到执行完毕的一个完整过程,也是进程本身从产生、发展到最终消亡的过程。
线程:(百度百科)
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程
中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线
程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight > processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
🍕10.1.2、多线程的运行机制
以往开发的程序大多是单线程的,即一个程序只有从开始到结束这一条执行路径。而多线程是指一个进程同时存在几条执行路径且并发执行的工作方式。
并发:
并发运行表示在一个处理器中,操作系统为了提高程序的运行效率,将 CPU 的执行时间分成多个时间片,分配给同一进程的不同线程。当执行完一个时间片后,当前运行的线程就可能交付出 CUP 权限,让其他线程执行下一个时间片,当然 CPU 也有可能将相邻的时间片分配给同一线程,即多个线程分享 CPU 时间,交替运行。
之所以从表面上看是多个线程同时运行的,是因为不同线程之间切换的时间非常短,也许仅仅是几毫秒,对普通人来说是难以感知的。
🍕10.1.3、多线程的优势
多线程优势:
1、充分利用 CPU 的资源:运行单线程程序时,若程序发生阻塞,则 CPU 可能会处于空闲状态,这将造成计算机资源浪费。而使用多线程可以在某个线程处于休眠或阻塞状态时运行其他线程,这样将大大提高资源利用率。
2、简化编程模型:可以考虑将一个既长又复杂的进程分为多个线程,成为几个独立执行的模块。
3、良好的用户体验:由于多个线程可以交替运行,减少或避免了程序阻塞或意外情况造成的响应过慢现象,减少了用户等待时间,提升了用户体验。
🍟10.2、多线程编程
Java 语言提供了 java.lang.Thread 类支持多线程编程。
🍕10.2.1、Thread 类介绍
Thread 类提供了大量的方法来控制和操作线程。
Thread 类常用方法:
方法 | 描述 | 类型 |
---|---|---|
Thread( ) | 创建 Thread 对象 | 构造方法 |
Thread(Runnable target) | 创建 Thread 对象,target 为 run( ) 方法被调用的对象 | 构造方法 |
Thread(Runnable target,String name) | 创建 Thread 对象,target 为 run( ) 方法被调用的对象, name 为新线程的名称 | 构造方法 |
void run( ) | 执行任务操作的方法 | 实例方法 |
void start( ) | 使该线程开始运行,JVM 将调用该线程的 run( ) 方法 | 实例方法 |
void sleep(long millis) | 在指定的毫秒数内让当前正在运行的线程休眠(暂停运行) | 静态方法 |
Thread currentThread( ) | 返回当前线程对象的引用 | 静态方法 |
主线程:
public static void main( ) 方法是主线程的入口,每个进程至少有一个主线程。重要性如下:
- 主线程是产生其他子线程的线程
- 主线程通常必须最后完成运行,因为它执行各种关闭动作。
尽管主线程是自动创建的,但是可以由一个 Thread 对象控制。
获取主线程信息:
public class MainThreadTest {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("当前线程:"+t.getName());
t.setName("MainThread");
System.out.println("当前线程:"+t.getName());
}
}
实现线程方式:
Java 中有两种实现线程的方式:
1、继承 Thread 类
2、实现 Runnable 接口
分别重写 run( ) 方法,执行代码写在 run( ) 方法中
🍕10.2.2、继承 Thread 类创建线程类
继承 Thread 类是实现线程的一种方式。
满足条件:
- 此类必须继承 Thread 类。
- 重写父类 run( ) 方法
- 将线程执行代码写在 run( ) 方法中。
如下实例:
// 继承 Thread 类的方式创建自定义线程类
public class MyThread extends Thread{
// 重写 run( ) 方法
public void run(){
// 线程执行任务的代码
}
}
// 启动线程的测试类
class ThreadTest{
public static void main(String[] args){
// 使用 start() 方法启动线程
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
🍕10.2.3、实现 Runnable 接口创建线程类
在使用继承 Thread 类的方式创建线程的过程中,子类无法再继承其他父类。因为 Java 不支持多继承。
可以通过实现 Runnable 接口的方式创建线程。这种方式更具有灵活性,用户线程还可以通过继承,再具有其他类的特性。
Runnable 接口位于 java.lang 包中,其中只提供了一个抽象方法 run( ) 声明,Thread 类也实现了 Runnable 接口。
如下实例:
// 实现 Runnable 接口方式创建线程类
public class MyThread implements Runnable{
// 重写 run( ) 方法
public void run(){
// 线程执行任务的代码
}
}
// 启动线程的测试类
class ThreadTest{
public static void main(String[] args){
// 使用 start() 方法启动线程
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
myThread.start(); // 启动线程
}
}
🍟10.3、线程的生命周期
新建的线程通常会在五种状态中转换,即:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。
画的好丑
1、新建状态:
一个 Thread 类或其子类的对象被声明并创建,但其在还未启动的这段时间里处于一种特殊的新建状态中。此时,线程类对象已经被分配了内存空间和资源,并且已被初始化,但是该线程尚未被调度。
2、就绪状态:
就绪状态也被称为可运行状态,处于新建状态的线程被启动后,也就是调用 start( ) 方法后,新建线程将进入线程队列,排队等待 CPU 时间片。此时它已具备运行的条件,一旦轮到它使用 CPU 资源,它就可以脱离创建它的主线程独立开始它的生命周期。
3、运行状态:
当就绪状态的进程被调度并获得处理器资源后便进入运行状态,该状态表示线程正在运行,该线程已经拥有了 CPU 的占用权。
处于运行状态的线程在一下四种情况下会让出 CPU 的控制权:
- 线程运行完毕
- 有比当前线程优先级更高的线程抢占了 CPU
- 线程休眠
- 线程因等待某个资源而处于阻塞状态。
4、阻塞状态:
一个正在运行的线程在某些特殊情况下需要让出 CPU 并暂时终止运行。这是,线程处于不可运行的状态被称为阻塞状态。
例如:遇到 sleep 线程休眠进入线程阻塞状态
5、死亡状态:
一个线程的 run( ) 方法运行完毕,表明该线程已死亡。处于死亡状态的线程不具有继续运行的能力。
导致线程死亡的原因有两个:
- 正常运行的线程完成了它的全部工作,即运行完 run( ) 方法的最后一条语句并退出;
- 当进程停止运行时,该线程中的所有线程将被强行终止。
线程处于死亡状态并且没有改线程的引用时,JVM 会从内存中删除该线程类对象。
🍟10.4、线程调度
🍕10.4.1、线程调度相关方法
常用的线程操作方法:
方法 | 描述 |
---|---|
int getPriority( ) | 返回线程的优先级 |
void setPriority(int newPriority) | 更改线程的优先级 |
boolean isAlive( ) | 测试线程是否处于活动状态 |
void join( ) | 进程中的其他线程必须等待该线程终止后才能运行 |
void interrupt( ) | 中断线程 |
void yield( ) | 暂停当前正在执行的线程类对象并运行其他线程 |
🍕10.4.2、线程的优先级
Thread 类提供了 setPriority(int newPriority) 方法,getPriority( ) 方法用于设置和返回指定线程的优先级,其中 setPriority(int newPriority) 方法参数 newPriority 可以是一个整数,范围是 1~10;
也可以使用 Thread 类的如下三个静态常量设置线程的优先级:
- MAX_PRIORITY:其值是 10 ,表示优先级最高。
- MIN_PRIORITY:其值是 1,表示优先级最低。
- NORM_PRIORITY:其值是 5,表示普通优先级。
🍟10.5、线程同步
线程异步会出现对数据访问的安全问题,所以需要让线程同步来保证数据的访问安全性。
🍕10.5.1、实现线程的同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这被称为线程同步。
🍕1、同步代码块
代码块即使用 “{ }”括起来的一段代码,使用 synchronized 关键字修饰的代码块被称为同步代码块。
语法:
synchronized(obj){
//需要同步的代码
}
如果一个代码块带有 synchronized(obj) 标记,那么当线程执行此代码块使,必须先获得 obj 变量所引向的对象的锁,其可以针对任何代码块,并且可以任意指定上锁的对象,因此灵活性更高。
🍕2、同步方法
如果一个方法的所有代码都属于需要同步的代码,那么这个方法定义处可以直接使用 synchronized 关键字修饰,即同步方法。
语法:
访问修饰符 synchronized 返回值类型 方法名(参数列表){
}
//或
synchronized 访问修饰符 返回值类型 方法名(参数列表){
}
🍟10.6、线程同步的特征
所谓线程之间保持同步,是指不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制,线程同步具有以下特征:
- 当多个并发线程访问同一对象的同步代码块或同步方法时,同一时刻只能有一个线程运行,其他线程必须等待当前线程运行完毕后才能运行。
- 如果多个线程访问的不是同一共享资源,则无需同步。
- 整篇文章属于学习笔记加本人知识,写了一个星期呜呜呜🤣。