数据
常量: 值不变, 有几种类型 (字符/字符串, 整数, 小数, 布尔值两个, 空值null)
变量: 某个范围内,值可以变,本质上是内存中的区域 数据类型 变量值 = 初始化值;
Int num = 123;
数据类型,就是限定变化范围, 内存空间大小
变量名:给这个区域起个名字
范围内要有内容,得有初始化值,值可以变化
注意: 变量没赋值,不能直接使用,因为变量的值有范围(数据类型,即内存大小), 可以先定义,再赋值,但是使用前一定要赋值, 不然使用时编译报错
数据类型:
内存大小怎么衡量? ,最小的存储单元B 字节byte, 八个比特位bit , 1024B = 1KB
数据类型实则是内存空间大小的区别,装不同的数据,(不同数据占的内存空间有大有小)
八大基本数据类型(byte,short,int,long,f,d,char,boolean) 整数4 小数2 字符1 布尔1
占的空间大小, 1,2,4,8/ 4,8/ 2,1 字节数Byte(8个位)
三大引用数据类型, class interface, [ ] 类 接口 数组
数据类型转换 隐式转换/自动提升 强制转换
数据做运算时,要求类型一致
如果不一致,
小类型自动提升为大类型,这是自动的,隐式的;
当把运算结果的大类型,赋值给小类型(大转小),要进行手动,强制类型转换,可能会有精度损失,超过了小类型的类型范围(内存空间)
赋值运算可以自带强转
运算
运算符, 对常量和变量进行操作的符号
整体上是运算表达式
算术 %取余,一般判断是否整除; 字符(‘a’ ‘A’)参与运算的是对应的值; 字符串参与加法运算是拼接; ++/-- 放在前面和后面有区别,在变量前面的话先自己++/--再运算,单独使用无所谓
赋值 = /=+ int a = 1 +2 ; 赋值运算
关系/比较运算 判断是否== > < 结果是boolean
逻辑 && || 用于连接布尔值/结果为布尔值的表达式(例如关系运算表达式)
三元 关系表达式? 表达式1:表达式2 ;
位与/逻辑与 短路与 (不能随意调换,有时候有意义)
短路与 的意义在于 如果第一个为false, 就不进行第二个的判断,有时候第二个的判断依存于第一个判断的结果为真
数组
数组本身是一种数据结构;
储存多个变量的容器,这些变量类型一致,长度确定了就不变
基本类型/引用类型都可以 //但存的话要存一致
定义数组 int[] arr =
初始化, 分配内存空间 new , 动态初始化/静态初始化
数组是引用数据类型 ,输出打印出来是 地址值 ,在堆里面 被 栈里面的变量所引用
引用的是地址,
获取数组的值, 通过索引 增删改查 只能改和查 根据索引 因为数组的长度一旦定义了就固定 容器 不能增删,数组定长
两个异常: 索引越界/空指针
方法
功能代码块,为了复用, 对实现过程的封装, 封装性的体现面向对象的基础
权限修饰符 返回值和参数列表
有无返回值 void
方法重载 多态性的体现,编译阶段
方法名相同(功能相同),参数列表不同(参数类型和参数个数), 与返回值类型无关
返回值并不是区分方法是否为同一个方法的标识, 只是运行后的状态
仅返回值不同,其他都相同,这被认为是同一个方法, 编译器不知道该调用哪个
传递参数
区别形参和实参,
实参是实际参与运算的参数,调用时要传进的实际参与运算的
在方法里写的(还没被调用,没被传参)是形式参数
传递基本数据类型时,形参改变不影响实参
传递引用数据类型时,形参改变直接影响实参,因为引用的地址没变,但是引用指向的内容发生了变化
值传递而非引用传递,java没有引用传递
传的是引用类型, 值就是对该对象的引用地址, 传递的是地址 , 变化的也是地址指向的内容
面向对象
面向对象思想:
跟面向过程相区分, 不关注实现过程, 把过程封装, 封装成方法, 方法封装成类, -----实例化为对象去使用
而是对象具有这个功能(方法),直接实现功能的调用
面向对象在程序中的体现,就是一个个封装好的类(类中封装函数,这些函数就是实现过程,把过程封装了,所以方法不能直接使用,必须通过类来使用,); 类实例化对象,调用对象去实现功能
封装
面向对象的基本原则, 把实现过程统统封装, 体现为方法/类, 实例化对象,调用方法
封装特性: 方法封装实现过程, 类封装成员变量和成员方法(事务的属性和行为),数据和方法 //方法其实是对数据的操作
面向对象的本质就在于把过程封装成类,然后调用类的对象
好处,隐藏细节,暴露接口,提高复用性
类/对象 => 类 依存于封装
类是某一类事务的抽象, 成员属性/成员方法, 描述这类事务的特征和行为 , 类不能直接使用,是总的概念,是统称,抽象的, 定义了事物的行为和特性,
成员属性是特征, 存储数据,
成员方法是行为, 处理数据,
对象是类的具体实例, 实例化的,具体存在的, 可以直接用 ,通过类的构造方法进行构造
This,当成员变量和局部变量同名, 进行区分, this.成员变量, 表示当前对象的成员变量(this,就是当前对象,哪个对象调用这个方法,这个this就是哪个对象,记录了其地址)
成员变量和局部变量区别: 变量还是以前那个变量的意思 其中成员分为静态和非静态
作用范围({}内,全局)/ 定义的位置/ 内存区域/ 初始值/ 生命周期(方法,类) 5点
注意:在函数中,如果局部变量和成员变量名字和类型都一致,那么函数会先使用局部变量。
就近原则
修饰符
public、和private关键字都属于java中的一种修饰符,用来修饰成员(成员变量、成员函数)的访问范围。
权限修饰符有四个public/protected/default/private
protected子类级别
Default包级别,同包才能访问
Private类级别,同类使用
Static静态/非静态修饰符, 修饰成员,与局部无关
Final 可修饰类, 方法, 变量
构造函数 : 一个方法,没有返回值/方法名与类名一致/参数列表写法跟普通方法一样
初始化值
创建对象/实例化对象时 会自动调用的函数,为了给对象的成员变量初始化值使用的
无参,什么都不做; 默认会有一个无参
有参,进行初始化操作,给成员变量赋值 或者调用方法??? 实例化进行的操作
在构造函数中可以去调用一般的函数,但是在一般的函数中不能调用构造函数。
一般方法不能调用构造方法,构造方法是可以调用一般的
构造方法之间可以互相调用,但调用形式跟一般不同,不是使用方法名来调用的,而是this.(参数列表),不能嵌套调用,那样会循环地无法停止地进行调用,this调用构造函数必须在第一句
1.1 继承的引入
案例 : 写一个学生类, 教师类, 校长类.
1.1.1 普通方式实现 :
知识点概述 :
- 私有化成员属性.
- 提供相应的 setter & getter 方法.
- 构造方法.
package cn.panda.demo1;
public class Student {
// 属性 (存储数据)
private String name;
private int age;
private char gender; // 'F'(female) / 'M'(male)
private String id; // panda_007
// 构造方法
public Student(String name, int age, char gender, String id) {
this.name = name;
this.age = age;
this.gender = gender;
this.id = id;
}
// 手动提供一个无参的构造方法, 方法创建对象
public Student() {
}
// 行为 (处理数据)
public void introduce() {
System.out.println("大家好, 我叫" + name + ", 我今年" + age + "岁了. " + gender);
System.out.println("我的学号为 : " + id);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
package cn.panda.demo1;
public class Teacher {
// 属性 (存储数据)
private String name;
private int age;
private char gender; // 'F'(female) / 'M'(male)
private String teachingField; // 教学领域 ("sport and english");
// 构造方法
public Teacher(String name, int age, char gender, String teachingField) {
this.name = name;
this.age = age;
this.gender = gender;
this.teachingField = teachingField;
}
// 手动提供一个无参的构造方法, 方法创建对象
public Teacher() {
}
// 行为 (处理数据)
public void introduce() {
System.out.println("大家好, 我叫" + name + ", 我今年" + age + "岁了. " + gender);
System.out.println("我的教学领域为 : " + teachingField);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
public String getTeachingField() {
return teachingField;
}
public void setTeachingField(String teachingField) {
this.teachingField = teachingField;
}
}
package cn.panda.demo1;
public class SchoolMaster {
// 属性 (存储数据)
private String name;
private int age;
private char gender; // 'F'(female) / 'M'(male)
private int workingYears; // 工作年限
// 构造方法
public SchoolMaster(String name, int age, char gender, int workingYears) {
this.name = name;
this.age = age;
this.gender = gender;
this.workingYears = workingYears;
}
// 手动提供一个无参的构造方法, 方法创建对象
public SchoolMaster() {
}
// 行为 (处理数据)
public void introduce() {
System.out.println("大家好, 我叫" + name + ", 我今年" + age + "岁了. " + gender);
System.out.println("我的工作年限为 : " + workingYears);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
public int getWorkingYears() {
return workingYears;
}
public void setWorkingYears(int workingYears) {
this.workingYears = workingYears;
}
}
问题 : 不同类中的代码重复.
说明 : 如果是同一个类中的代码重复, 此时就可以将相同的代码抽取为独立方法, 实现调用即可.
如何解决 : `继承`, 继承语句是属于类的. 是类在继承. 把不同类中重复的部分抽取出来作为父类,各个类直接继承该父类,即拥有父类的属性和方法
面对对象的思想,即是对类的封装(属性:存储数据,方法:处理数据);通过对象去使用类.
Main方法(测试类)是执行者,是运行和启动虚拟机的,在栈区进行
同一类的代码重复,用方法进行共性抽取,不同类中的代码重复,使用继承,继承是类之间的关系.
1.1.2 继承方式实现 :
继承是 类在继承, 不是对象在对象. 在我们设计类的同时, 需要思考类之间的继承关系.
继承解决什么问题 : 继承解决了不同类中重复代码的问题.
继承的核心 : 子类无条件拥有父类中所有可继承的属性和方法. 可继承指哪些?构造方法不可继承,其他成员都可继承
package cn.panda.demo2;
public class Person {
// 属性
// private (访问权限)私有化 : 对内开放, 对外保护.
private String name;
private int age;
private char gender;
// 行为
public void introduce() {
System.out.println("大家好, 我叫" + name + ", 我几年" + age + "岁了. " + gender);
}
// 间接方法私有属性 setter & getter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
}
package cn.panda.demo2;
public class Student extends Person {
// 属性
private String id;
// setter & getter
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
package cn.panda.demo2;
public class Teacher extends Person {
// 属性
private String teachingField;
public String getTeachingField() {
return teachingField;
}
public void setTeachingField(String teachingField) {
this.teachingField = teachingField;
}
}
package cn.panda.demo2;
public class SchoolMaster extends Person {
// 属性
private int workingYears;
public int getWorkingYears() {
return workingYears;
}
public void setWorkingYears(int workingYears) {
this.workingYears = workingYears;
}
}
存在一些小问题 :
- 子类没有输出自己类中定义的特有属性 (Student->id Teacher->teachingField SchoolMaster->workingYears)
- 子类没有定义 `全参` 构造方法, 因此在程序中, 我们使用 setter 方法为对象赋值.
1.2 继承的练习
案例 : 继承的代码实现 (掌握)
- 说明 : Animal 是父类, Dog 和 Cat 都是 Animal 的子类. ArmyDog(军犬) 是 Dog 的子类.
package cn.panda.demo3;
public class Animal {
// 属性
private String name;
private int age;
// 行为
public void run() {
System.out.println(age + "岁的" + name + "正在疯狂裸奔中...");
}
public void eat() {
System.out.println(age + "岁的" + name + "正在疯狂进食中...");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package cn.panda.demo3;
public class Cat extends Animal {
// 属性
// 特有行为 : 抓老鼠
public void catchMouse() {
System.out.println(getAge() + "岁的" + getName() + "正在疯狂抓老鼠...");
}
}
package cn.panda.demo3;
public class Dog extends Animal {
// 属性
// 特有行为 : 保护家
public void protectedHome() {
System.out.println(getAge() + "岁的" + getName() + "正在疯狂进食中...");
}
}
package cn.panda.demo3;
// ArmyDog 类和 Animal 类是什么关系 ??? 多重继承关系.
public class ArmyDog extends Dog {
// 属性
// 行为
public void bombBlockhouse() {
System.out.println(getAge() + "岁的" + getName() + "正在疯狂炸碉堡中...");
}
}
package cn.panda.demo3;
public class TestAnimal {
public static void main(String[] args) {
// 1. Dog
Dog dog = new Dog();
dog.setName("旺财");
dog.setAge(2);
dog.run();
dog.eat();
// 自己的
dog.protectedHome();
System.out.println("--------------------");
// 2. Cat
Cat cat = new Cat();
// 名言 : 要节约用水,尽量和女友一起洗澡.
cat.setName("加菲猫");
cat.setAge(18);
cat.eat();
cat.run();
cat.catchMouse();
System.out.println("--------------------");
// 3. 军犬 (裸奔, 进食, 保护家, 炸碉堡)
ArmyDog armyDog = new ArmyDog();
armyDog.setName("哮天犬");
armyDog.setAge(100);
armyDog.eat();
armyDog.run();
armyDog.protectedHome();
armyDog.bombBlockhouse();
}
}
继承的注意点 :
- Java语言的继承是 `单继承`, 也就是说, 一个类只能继承一个父类.
- Java语言可以实现 `多重继承`. C extends B, B extends A. C类和A类之间就是多重继承的关系.
1.3 面向对象-方法重写 Override
代码案例 : 有一个 "人" 类.拥有一个sayHi的方法. 又有三个子类,"中国人", "韩国人", "日本人". 三个子类都各自重写了sayHi方法.因为三个子类对同一种行为都有其不同的表现形式.同样的功能表述,不同的执行内容
package cn.panda.demo4;
public class Person {
// 属性
private String name;
// 行为
public void sayHi() {
System.out.println(name + "说: 大家好, 我是人.");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package cn.panda.demo4;
public class Chinese extends Person {
}
package cn.panda.demo4;
public class Korean extends Person {
}
package cn.panda.demo4;
public class Japanese extends Person {
}
package cn.panda.demo4;
public class TestPerson {
public static void main(String[] args) {
// 1. Chinese
Chinese c = new Chinese();
c.setName("伟大的中国人");
c.sayHi();
// 2. Korean
Korean k = new Korean();
k.setName("韩国美眉");
k.sayHi();
// 3. Japanese
Japanese j = new Japanese();
j.setName("苍老师");
j.sayHi();
}
}
问题 :子类继承父类, 确实拥有了 `属性和行为`. 但是, 子类的行为, 如果和父类实现不一样, 如何处理 ???
解决 : 子类重写父类中继承而来的方法. override
重写方法的注意点 :
- 方法名必须与父类中继承而来的方法名称一模一样.
- 方法的参数列表也要保持一致.
- 方法的返回值也要相同.
子类重写的方法访问权限修饰符必须要 `大于等于` 父类中重写的方法.
public(公共访问级别) > protected(子类访问级别) > 默认(包级别) > private (私有化)
父类被重写的方法不能是private修饰,被private修饰,子类没有访问权限,无法进行重写(子类若写了同名的方法,也不叫方法重写,).
成员变量不存在重写. 只对成员方法
说明 : 如果子类重写了父类中定义的方法, 程序执行时, 执行子类重写的方法.
package cn.panda.demo4;
public class Chinese extends Person {
// 重写 sayHi() 方法
public void sayHi() {
System.out.println(getName() + "说: 你吃了吗???");
}
}
package cn.panda.demo4;
public class Korean extends Person {
// @Override 编译检查, 编译器会帮助我们检查下面这个方法是否为父类中定义的方法. 如果是, 不报错, 如果不是, 编译报错.
@Override
public void sayHi() {
System.out.println(getName() + "说: 阿尼哈斯哟, 泡菜思密达...");
}
}
package cn.panda.demo4;
public class Japanese extends Person {
@Override
public void sayHi() {
System.out.println(getName() + "说: 亚麻得, 八嘎...");
}
}
package cn.panda.demo4;
public class TestPerson {
public static void main(String[] args) {
// 1. Chinese
Chinese c = new Chinese();
c.setName("伟大的中国人");
c.sayHi();
// 2. Korean
Korean k = new Korean();
k.setName("韩国美眉");
k.sayHi();
// 3. Japanese
Japanese j = new Japanese();
j.setName("苍老师");
j.sayHi();
}
}
研究 : Japanese 类 :
package cn.panda.demo4;
public class Japanese extends Person {
// 需求 : 希望执行父类中该方法的 `代码逻辑`.
@Override
public void sayHi() {
// 说明 : 该逻辑父类中已经存在, 子类不应该再重新写一遍.
// System.out.println(getName() + "说: 大家好, 我是人.");
// 解决方案 : super 父类
// 含义 : 调用父类中的 sayHi() 方法.
super.sayHi(); // 该语句子类是可选的, 而且调用的位置可以改变.
System.out.println(getName() + "说: 亚麻得, 八嘎...");
}
}
1.4. super 关键字 (子父类中构造方法的特点)
Super关键字三种场景:
方法重写后,子类调用方法会执行子类重写后的方法,这时候若想执行父类的被重写的方法,使用super.方法名();
调用父类的构造函数时,父类的属性,父类的构造器进行初始化.super(参数);
成员变量的重名(子类与父类进行区分,this/super)
package cn.panda.demo5;
// 定义了一个爷爷类
public class Grandpa {
// 属性
private String name;
private int age;
private char gender;
/*
* 在每一个构造方法中, 第一条语句就是 super(); 如果将其删除, 默认依然存在.
*/
public Grandpa(String name, int age, char gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
}
public Grandpa() {
super();
System.out.println("调用了 Grandpa() 无参构造方法...");
}
// 定义一个 `介绍自己` 的方法
public void introduce() {
System.out.println("大家好, 我叫" + name + ",我今年" + age + "岁了. " + gender);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
}
package cn.panda.demo5;
public class Father extends Grandpa {
// 属性
private int money;
public Father(String name, int age, char gender, int money) {
// 说明1 : 父类的属性交给父类进行初始化
super(name, age, gender);
// 说明2 : 自己的属性自己初始化
this.money = money;
}
public Father() {
super();
System.out.println("调用了 Father() 无参构造方法...");
}
// 重写 introduce() 方法
@Override
public void introduce() {
super.introduce();
System.out.println("我有钱: " + money);
}
// 行为
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
}
package cn.panda.demo5;
public class Son extends Father {
// 属性
private String knowledge; // `知识/证书` 改变命运
// 行为
public Son(String name, int age, char gender, int money, String knowledge) {
// 说明 : 将父类中继承而来的属性, 传递给父类进行初始化.
super(name, age, gender, money);
// 说明 : 自己的属性自己初始化.
this.knowledge = knowledge;
}
public Son() {
// Constructor call must be the first statement in a constructor
// 构造方法的调用在构造方法中, 必须是第一条执行语句.
super();
System.out.println("调用了 Son() 无参构造方法...");
}
// 重写 introduce 方法
@Override
public void introduce() {
super.introduce();
System.out.println("我的知识是 : " + knowledge);
}
public String getKnowledge() {
return knowledge;
}
public void setKnowledge(String knowledge) {
this.knowledge = knowledge;
}
}
1.5 综合练习 :
案例 : 写一个学生类, 教师类, 校长类.
属性 : 姓名, 年龄, 性别. Student (学号), Teacher (教学领域), SchoolMaster (工作年限)
行为 : 介绍自己.
package cn.panda.demo6;
public class Person {
// 属性
private String name;
private int age;
private char gender;
// 构造方法
public Person(String name, int age, char gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
}
// 问题 : 如果父类不提供无参构造方法, 子类会发生什么情况 ???
//对于子类而言,没有super()了,子类会无法创建(子类无法通过无参构造继承父类的属性), 子类非要创建,前提是提供含有 super(父类属性) 的构造方法
子类对象初始化会调用父类的构造函数.
// 说明 : 父类提供无参构造方法目的就是为了方便 `子类的创建`,为子类继承父类的属性进行初始化
public Person() {
super();
}
// 行为 :
public void introduce() {
System.out.println("大家好, 我叫" + name + ", 我今年" + age + "岁了. " + gender);
}
// setter & getter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
}
package cn.panda.demo6;
public class Student extends Person {
// 属性
private String id;
// 构造方法
public Student(String name, int age, char gender, String id) {
super(name, age, gender);
this.id = id;
}
// 重写 introduce() 方法
@Override
public void introduce() {
// 需要调用父类中的该方法代码逻辑
super.introduce();
// 自己的业务逻辑
System.out.println("我的学号为 : " + id);
}
public Student() {
super();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
package cn.panda.demo6;
public class TestStudent {
public static void main(String[] args) {
// 1. 创建一个 Student 对象
Student stu = new Student();
stu.introduce();
System.out.println("-------------");
// 2. 创建一个 Student 对象
Student stu2 = new Student("张三", 18, '男', "panda_007");
stu2.introduce();
}
}
1.6 继承的注意点 :
1. 继承是类在继承, 与对象无关.
2. 继承一定要满足 is a 关系.
Student extends Person; Student is a Person.
Teacher extends Person; Teacher is a Person.
Cat extends Animal; Cat is a Animal;
ArmyDog extends Dog; ArmyDog is a Dog;
3. 继承一定要合理性, 千万不要为了继承而继承.
4. 父类中的属性和行为一定是 `共性的`. 所有的子类都应该具备的. 这样的属性和行为才可以被定义在父类中. 如果有一个属性或行为不是所有子类都共有的, 那么该属性和行为就不应该被定义在父类中.(可以选择接口,)
子类继承父类中所有能被继承的:
构造方法不能被继承(构造方法需要与类名相同,子类需要有自己类的构造方法),其他都可以,
至于私有的成员也可以继承,但限制子类直接访问,访问通过其他公共的方法(getter/setter)进行. private只能本类访问
继承
1. 在一个类的基础上构建新的类, 新的类可以继承这个类的属性和方法, 子类父类概念;
2. 解决代码重复问题, 类和类之间的代码重复, 提高复用性的同时, 而且是对封装的扩展,类已经封装好了,对类进行扩展
3. 无条件继承,所有可继承的属性和方法; 这里是排除构造方法,不能继承,(构造方法是实例化对象使用的, 子类应该有自己的构造方法, 而不是继承父类
4. 单继承? 只能有一个直接父类 //可以多重继承 如果多继承,两个父类有相同的属性/方法,使用时都不知道该找哪个了
5. 注意
继承要合理, 类之间有共同成员属性/方法 is a 关系 有从属关系
定义父类的时候, 属性/方法要有共通性,因为会被所有继承了的子类所拥有 (如果一个类要作为父类)
方法重写
子类对继承的来自父类的方法进行重新实现, 方法名/参数列表/返回值都一样,实现体(功能代码)不一样 (虽然都是这个行为,子类的这个行为和父类这个行为的内容不一样), 访问权限只能大不能小,不能有更多异常
同样的功能表述,但是不同的执行内容
构造方法不能重写,//都没继承到
Final不能重写 //最终,不可修改
Private修饰的方法不能重写 //子类无法访问
Static的不能重写 //直接通过类进行调用了,与对象无关 ???
子类调用这个方法时,如果重写过,就调用自己的,没重写过,就还是调父类的,如果重写过了而还是要使用父类的该方法,加super关键字进行调用
(关于属性不存在重写的问题, 定义了相同属性名,那也只是相同名的不同属性,不属于继承)
子类的构造方法会先调用父类的构造方法(子类是没有继承父类的构造方法的,所以需要super()调用父类的构造方法对父类中的成员进行初始化,才好继承) , super(); 空参构造/有参构造(前提是父类有该有参构造), 隐式的是无参的,有参需要自己手动;
所以每个类都应该保留/定义无参构造方法,给子类继承时调用
调用时,必须放在子类构造方法里第一句
2. 面向对象 – 多态 :
请问 : 什么是多态 ???
程序的理解 : 父类的引用指向了子类的对象.这就是多态.
生活的理解 : 对于同一种行为(cut), 根据传入的不同对象, 产生不同的执行结果.
多态的继承是 `继承`.
package cn.panda.demo1;
public class Person {
// 属性
private String name;
// 行为
public void doSomething() {
System.out.println(name + "正准备做: XXXOOO ...");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package cn.panda.demo1;
public class Barber extends Person {
// 重写 doSomething 行为
@Override
public void doSomething() {
System.out.println(getName() + "正准备认真细致地给顾客剪头发...");
}
}
package cn.panda.demo1;
public class Actress extends Person {
@Override
public void doSomething() {
System.out.println(getName() + "正准备停止表演...");
}
}
package cn.panda.demo1;
public class Doctor extends Person {
@Override
public void doSomething() {
System.out.println(getName() + "正准备给你开膛破肚...");
}
}
package cn.panda.demo1;
public class TestPerson {
public static void main(String[] args) {
// 1. Barber
Barber b = new Barber();
b.setName("理发师");
// 2. Actress
Actress a = new Actress();
a.setName("女演员");
// 3. Doctor
Doctor d = new Doctor();
d.setName("女医生");
cut(d);
}
// 定义一个方法 : cut
public static void cut(Barber b) {
b.doSomething();
}
public static void cut(Actress a) {
a.doSomething();
}
public static void cut(Doctor d) {
d.doSomething();
}
}
问题 : 同一个 cut 行为, 写了很多遍. (重载语法实现)
分析 : Barber, Actress, Doctor 都是 Person 对象. Person 未来可能会有 `很多` 对象. 因此就造成了方法的大量重复.
2.1 多态的第一种使用场景 (方法参数设计)
package cn.panda.demo1;
public class TestPerson {
public static void main(String[] args) {
// 1. Barber
Barber b = new Barber();
b.setName("理发师");
// 2. Actress
Actress a = new Actress();
a.setName("女演员");
// 3. Doctor
Doctor d = new Doctor();
d.setName("女医生");
cut(b);
}
// 定义一个方法 : cut
// 多态的第一种使用场景 : 在设计一个方法参数时, 尽量将参数设计为 `父类引用`, 因为父类引用可以接收所有的子类对象.
public static void cut(Person p) {
// Person p = new Doctor(); 多态 : 父类引用指向了子类对象.
// Person p = new Actress();
// Person p = new Barber();
p.doSomething();
}
}
2.2 多态的第二种使用场景 (对象引用接收)
问题 : 一旦程序更换对象, 代码的修改量非常大, 说明程序的维护性差.
package cn.panda.demo1;
public class TestPerson {
public static void main(String[] args) {
// 多态 : 父类引用指向了子类对象.
// 多态的第二种使用场景 : 创建子类对象, 使用父类引用接收, 可以提高程序的维护性.
Person p = new Actress();
p.setName("女演员");
cut(p);
}
// 定义一个方法 : cut
// 多态的第一种使用场景 : 在设计一个方法参数时, 尽量将参数设计为 `父类引用`, 因为父类引用可以接收所有的子类对象.
public static void cut(Person p) {
// Person p = new Doctor();
// Person p = new Actress();
// Person p = new Barber();
p.doSomething();
}
}
2.3 多态的弊端 :
多态 不能使用子类特有的方法 (父类没有子类特有的方法,无法进行调用,编译不通过)
如果要使用,需要进行类型向下强制转,
多态中:
多态形式 对于成员方法:如果是静态方法(父类和子类都具有此静态方法),执行父类的,因为是静态,与对象无关,左边编译通过就执行;如果是非静态方法,子类有执行子类,子类没有就执行父类
多态形式 对于成员变量:永远执行父类(字段不存在多态,不存在重写)
步骤一 : 给三个子类对象增加其 `特有的行为`.
- 理发师 : chasingGirls(); 追女孩.
- 女演员 : beFamous(); 成名.
- 女医生 : temptationOfUniforms(); 制服诱惑.
package cn.panda.demo1;
public class TestPerson2 {
public static void main(String[] args) {
// 1. Barber
Barber b = new Barber();
b.setName("理发师");
b.doSomething();
b.chasingGirls();
// 2. Actress
Actress a = new Actress();
a.setName("女演员");
a.doSomething();
a.beFamous();
// 3. Doctor
Doctor d = new Doctor();
d.setName("女医生");
d.doSomething();
d.temptationOfUniforms();
}
}
package cn.panda.demo1;
public class TestPerson {
public static void main(String[] args) {
// 多态 : 父类引用指向了子类对象. (向上转型 )
// 多态的第二种使用场景 : 创建子类对象, 使用父类引用接收, 可以提高程序的维护性.
Person p = new Actress();
p.setName("女演员");
cut(p);
}
// 定义一个方法 : cut
// 多态的第一种使用场景 : 在设计一个方法参数时, 尽量将参数设计为 `父类引用`, 因为父类引用可以接收所有的子类对象.
public static void cut(Person p) {
p.doSomething();
// 子类特有行为 :
// 报错 : beFamouse() 方法没有在 Person 类中被定义. 因为 p 调用是 Person 类型的.
// 说明 : 编译器是根据 `对象` 的接收类型类寻找 `属性和方法` 的.
// 解决方法 : 对象类型 `向下转型`. 将父类引用转换为子类引用.
Actress a = (Actress) p;
a.beFamous();
}
}
说明 : 创建的对象与向下转型的类型保持了一致, 因此转型成功, 特有方法也被执行成功了.
请问 : 如果创建的对象与向下转型类型没有保持一致, 会发生什么情况 ???
编译阶段 : 一切正常.
运行阶段 : ClassCastException 类型转换异常. Actress 不能被转换为 Doctor.
说明 : 向下转型有风险, 使用需谨慎.
Java 语言提供了一个关键字 : instanceof
作用 : 判断对象的具体类型.
格式 :对象 instanceof 类型 翻译: 判断左边对象是否为右边类型的对象 ??? true / false
package cn.panda.demo1;
public class TestPerson {
public static void main(String[] args) {
// 多态 : 父类引用指向了子类对象. (向上转型 )
// 多态的第二种使用场景 : 创建子类对象, 使用父类引用接收, 可以提高程序的维护性.
Person p = new Doctor();
p.setName("女医生");
cut(p);
}
// 定义一个方法 : cut
// 多态的第一种使用场景 : 在设计一个方法参数时, 尽量将参数设计为 `父类引用`, 因为父类引用可以接收所有的子类对象.
public static void cut(Person p) {
// 说明: 父类中共性的方法, 无需向下转型, 如果程序运行时, 子类重写了就调用子类重写的方法. 如果没有重写就调用方法中继承而来的方法.
p.doSomething();
// 特有行为 :
// 报错 : beFamouse() 方法没有在 Person 类中被定义. 因为 p 调用是 Person 类型的.
// 说明 : 编译器是根据 `对象` 的接收类型类寻找 `属性和方法` 的.
// 解决方法 : 对象类型 `向下转型`. 将父类引用转换为子类引用.
// boolean result = p instanceof Doctor;
// System.out.println(result);
// 多态中子类特有的方法解决方案 (多态弊端的解决方案)
// 向下转型的目的 : 为了调用子类特有的 `属性/方法`.
if (p instanceof Doctor) {
Doctor d = (Doctor) p;
d.temptationOfUniforms();
} else if (p instanceof Actress) {
Actress a = (Actress) p;
a.beFamous();
} else if (p instanceof Barber) {
Barber b = (Barber) p;
b.chasingGirls();
}
}
}
多态
同样的对象操作同样的方法,达到不同的效果 //同样的功能表述,但是不同的功能效果
方法重载 和 方法重写都是多态性的体现,
方法重载(编辑阶段的多态性)
方法重写(运行阶段的多态性),这也是面向对象的精髓之处, 有两点:方法重写,父类引用指向子类对象
父类的引用指向子类对象/实现类对象
拓展性很强,父类引用作为方法参数传入,传入不同的子类/实现类,就有不同的效果;
维护性好,定义子类对象,用父类引用接收
弊端:如果调用父类没有而子类特有的方法,编译不会通过,这时需要进行强转
3. final 关键字 :
final : 最终化, 无法再次更改的含义.
中国(China)、俄罗斯(Russia)、巴西(Brazil)、 印度(India)、 南非(South Africa)
package cn.panda.demo2;
public class Animal {
// 属性
// 行为 : 联合声明
// 要求 : 希望所有子类都可以继承该行为, 并使用该行为. 但是不允许子类 `重写 / 篡改` 该行为.
public void decleration() {
System.out.println("动物类发表的联合声明 : 我们动物是人类的好朋友.");
}
}
package cn.panda.demo2;
public class Fox extends Animal {
// 重写了父类的方法
@Override
public void decleration() {
System.out.println("动物们发表的联合声明: 我们动物是人类的天敌...");
}
}
package cn.panda.demo2;
public class TestDecleration {
public static void main(String[] args) {
// 多态语法 : 父类引用指向了子类对象
Animal a = new Fox();
a.decleration();
}
}
解决方案 : 使用 final 关键字修饰父类中的 `联合声明` 方法.
package cn.panda.demo2;
public class Animal {
// 属性
// 行为 : 联合声明
// 要求 : 希望所有子类都可以继承该行为, 并使用该行为. 但是不允许子类 `重写 / 篡改` 该行为.
public final void decleration() {
System.out.println("动物类发表的联合声明 : 我们动物是人类的好朋友.");
}
}
说明 : final 可以修饰方法, 被 final 修饰的方法, 子类可以继承, 也可以使用, 但是子类不能重写.
package cn.panda.demo2;
public class Fox extends Animal {
// 重写了父类的方法
// Cannot override the final method from Animal
// 不能重写被final修饰符的方法
/* @Override
public void decleration() {
System.out.println("动物们发表的联合声明: 我们动物是人类的天敌...");
} */
}
final 关键字也可以修饰类 : 被final 修饰的类无法被继承.
package cn.panda.demo2;
public final class Animal {
// 属性
// 行为 : 联合声明
// 要求 : 希望所有子类都可以继承该行为, 并使用该行为. 但是不允许子类 `重写 / 篡改` 该行为.
public final void decleration() {
System.out.println("动物类发表的联合声明 : 我们动物是人类的好朋友.");
}
}
final 关键字修饰 `属性(成员变量) / (局部变量)` :
属性 :
局部 :
package cn.panda.demo2;
// The type Dog cannot subclass the final class Animal
// Dog 不能成为被 final 修饰 Animal 的子类.
public class Dog extends Animal {
// 属性
// The blank final field age may not have been initialized
// 被 final 修饰的空属性 age, 可能没有被初始化.
// 说明1 : 不断更改的属性, 如果使用 final 修饰是非常不合理的. (name, age, gender, score ...)
private /*final*/ int age = 18;
// 说明2 : 不可更改的属性就应该使用 final 修饰 (public static final 组合关系)
public static final double PI = 3.14;
// 行为 :
public void show() {
// The final field Dog.age cannot be assigned
// 被 final 属性的 age 属性不可以再次赋值.
// age = 100;
// PI = 10.88;
// 局部变量 (没有默认值)
final int num1; // = 10;
num1 = 20;
// num1 = 100;
}
}
重点 : 开发中一定会使用到 final 修饰方法和成员属性.
4. 抽象类 :
4.1 抽象类的引入
package cn.panda.demo3;
// The abstract method shout in type Animal can only be defined by an abstract class
// 说明2 : 如果一个类拥有抽象方法, 那么该类就必须被定义为 `抽象类`.
public abstract class Animal {
// 属性
private String name;
// 行为 :
// shout()
// This method requires a body instead of a semicolon
// 这个方法需要一个方法体, 而不是分号.
// 说明1 : 没有方法体的方法必须定义为 `抽象方法`. 抽象方法使用 abstract 关键字修饰.
public abstract void shout();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4.2 抽象类的使用
Animal 类定义抽象方法的目的是什么 ??? 为了给所有的子类定义 `行为规范`.
说明 : 如果一个类继承 Animal 类, 该类如果想要被实例化, 那么该子类就必须重写 Animal 类中的全部 `抽象方法`.
package cn.panda.demo3;
// The abstract method shout in type Animal can only be defined by an abstract class
// 说明2 : 如果一个类拥有抽象方法, 那么该类就必须被定义为 `抽象类`.
public abstract class Animal {
// 属性
private String name;
// 行为 :
// shout()
// This method requires a body instead of a semicolon
// 这个方法需要一个方法体, 而不是分号.
// 说明1 : 没有方法体的方法必须定义为 `抽象方法`. 抽象方法使用 abstract 关键字修饰.
// 父类 Animal 类认为所有的子类都应该拥有 `shout 和 eat` 方法, 但是父类不知道这些方法的具体代码实现, 因此将方法定义为 `抽象方法`.
// 要求子类必须全部重写这些方法. 我们就可以认为父类定义抽象方法的目的就是为了给子类定义 `行为规范`.
public abstract void shout();
public abstract void eat();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4.3 抽象类中的非抽象方法
package cn.panda.demo3;
// The abstract method shout in type Animal can only be defined by an abstract class
// 说明2 : 如果一个类拥有抽象方法, 那么该类就必须被定义为 `抽象类`.
public abstract class Animal {
// 属性
private String name;
// 行为 :
// This method requires a body instead of a semicolon
// 这个方法需要一个方法体, 而不是分号.
// 说明1 : 没有方法体的方法必须定义为 `抽象方法`. 抽象方法使用 abstract 关键字修饰.
// 父类 Animal 类认为所有的子类都应该拥有 `shout 和 eat` 方法, 但是父类不知道这些方法的具体代码实现, 因此将方法定义为 `抽象方法`.
// 要求子类必须全部重写这些方法. 我们就可以认为父类定义抽象方法的目的就是为了给子类定义 `行为规范`.
public abstract void shout();
public abstract void eat();
public void run() {
System.out.println(name + "正在疯狂裸奔中...");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package cn.panda.demo3;
public class Dog extends Animal {
@Override
public void shout() {
System.out.println(getName() + " : 汪汪汪 ...");
}
@Override
public void eat() {
System.out.println(getName() + "说 : 跟我混, 有骨头吃 ...");
}
}
package cn.panda.demo3;
public class Cat extends Animal {
@Override
public void shout() {
System.out.println(getName() + " : 喵喵喵 ...");
}
@Override
public void eat() {
System.out.println(getName() + "说 : 有鱼吃 ...");
}
}
package cn.panda.demo3;
public class Wolf extends Animal {
@Override
public void shout() {
System.out.println(getName() + " : 嗷嗷嗷 ...");
}
@Override
public void eat() {
System.out.println(getName() + "说 : 跟我混, 有肉吃 ...");
}
}
package cn.panda.demo3;
public class TestAnimal {
public static void main(String[] args) {
// 多态 : 创建子类, 使用父类引用接收, 调用父类中定义的 `行为规范`.
Animal a = new Wolf();
a.setName("灰太狼");
a.run();
// 编译器认为 a 是 Animal 类型的. 所有在编译阶段 shout() / eat() 会去 Animal 类中找.
a.shout();
a.eat();
}
}
抽象类有构造函数
因为抽象类中除了抽象函数还有其他非抽象函数以及成员属性,子类继承抽象类,继承抽象类的成员变量,需要构造函数为成员变量进行初始化;抽象类虽然有构造方法,但不能实例化
4.4 abstract 关键字的补充 :
问题 : abstract 关键字不能与那些修饰符共同使用 ???
访问权限修饰符 : private < 默认 < protected < public
静态非静态 : static / non-static
最终化 : final
作用 : 父类定义 abstract 方法的目的就是为了给子类定义行为规则, 这些行为规则要求子类必须重写.
private 私有化, 子类都看不见了, 如何重写呢 ??? 语法错误!
static 关键字修饰的属性和方法, 可以直接使用类名调用.
final 表示最终化, 与 abstract 关键字的含义冲突了.
说明 : Animal 是抽象类, 子类如果没有全部重写父类中的抽象, 那么该子类就是一个抽象类, 抽象类不能实例化对象.
package cn.panda.demo3;
// The type Sheep must implement the inherited abstract method Animal.eat()
// 指定的 sheep 类必须重写从 Animal 类中继承而来的 eat() 抽象方法.
// Sheep 类如果不重写 eat() 抽象方法, 就意味着该类就存在一个抽象方法.
// 注意点 : 拥有抽象方法的类, 必须定义为 `抽象类`.
public abstract class AbstractSheep extends Animal {
@Override
public void shout() {
System.out.println(getName() + " : `咩咩咩` ...");
}
}
package cn.panda.demo3;
public class Sheep extends AbstractSheep {
@Override
public void eat() {
System.out.println(getName() + " : 吃草 ...");
}
}
package cn.panda.demo3;
public class TestAnimal2 {
public static void main(String[] args) {
// Animal -> AbstractSheep -> Sheep
Animal a = new Sheep();
a.setName("喜洋洋");
a.run();
a.eat();
a.shout();
}
}
抽象
1. 有抽象方法的类就被定义为抽象类, 抽象方法没有实现体, 因此抽象类无法实例化(因为对象会调用方法,没有实现体的方法无意义)
2. 抽象类的意义在于,给子类继承和实现,为子类定义规范,子类也只有实现了所有的抽象方法才能实例化 意义就在制定规范,给子类继承和实现
3. 对一类事务进行抽象,抽象成类,定义规范,不关注细节,让子类去实现 //数据抽象/行为抽象 //子类继承抽象类,
有构造方法,但也不能实例化,构造方法在于为抽象类里的非抽象方法进行初始化
抽象类中可以有静态方法(这个静态方法要是非抽象的方法,抽象类可以有非抽象方法)
abstract关键字不能和static/final/private一起用
5. 接口 :
为所有需要接口中功能行为的类定义统一的行为规范(功能描述),接口不属于父子类体系,可以理解为不具备共性,而是额外,是拓展,所以不能抽取到父类,因此接口是面向所有类,谁需要这项功能,就来实现这套规则
(抽象类是父子类,给子类制定规范,子类继承,且需要实现抽象方法)
解耦: 接口将定义与实现分离,解开类与类之间的耦合性,增强了拓展性
接口作为方法参数传入,传入的接口为任意实现类对象(接口作为中间件,完成了调用接口的主体与接口实现类的解耦)
接口作为方法返回值,返回的是接口的任意实现类对象
接口与类不同:
成员位置上只有常量(public static final)和抽象方法(public abstract);没有构造函数,与对象无关,
接口是一种比抽象类更为抽象的存在. 是功能的集合
因为都是抽象函数,接口和接口之间可以多继承,接口和实现类之间也可以多实现
实现类需要重写/实现接口中所有的抽象方法,不然实现类是抽象类,无法实例化
多用接口,少用抽象类,除非要为子类提供共性的功能(非抽象的共性方法)
5.1 接口在程序中的使用
5.2 多态在接口中的体现 (缉毒方法的设计)
package cn.panda.demo1;
public interface BlindGuidable {
// 常量 (默认修饰符 : public static final) 被final修饰的属性 `全部大写`.
int YEARS = 2;
// Abstract methods do not specify a body 抽象方法不能指定方法体.
// 抽象方法 (默认修饰符 : public abstract)
void blindGuiding();
}
package cn.panda.demo1;
public interface DrugDetectable {
// 常量 (默认修饰符 : public static final)
int YEARS = 5;
// 抽象方法 (默认修饰符 : public abstract)
void drugDetection();
}
package cn.panda.demo1;
public class Dog extends Animal implements BlindGuidable, DrugDetectable {
@Override
public void shout() {
System.out.println(getName() + " : 汪汪汪 ...");
}
@Override
public void eat() {
System.out.println(getName() + "说 : 跟我混, 有骨头吃 ...");
}
@Override
public void blindGuiding() {
System.out.println("经过" +BlindGuidable.YEARS + "年的训练," + getName() + "具备了导盲的功能...");
}
@Override
public void drugDetection() {
System.out.println("经过" +DrugDetectable.YEARS + "年的训练," + getName() + "具备了缉毒的功能...");
}
}
package cn.panda.demo1;
public class TestAnimal {
public static void main(String[] args) {
// 使用 :
Animal a = new Dog(); //多态接受
a.setName("哮天犬");
a.run();
a.shout();
a.eat();
Dog dog = (Dog) a;
dog.blindGuiding();
dog.drugDetection();
}
}
package cn.panda.demo1;
public class TestAnimal2 {
public static void main(String[] args) {
Dog dog = new Dog(); //没用多态
dog.setName("哮天犬");
dog.blindGuiding();
dog.drugDetection();
}
}
5.3 接口的练习
5.4 接口与类的关系总结 :
- 接口和接口 : 继承的关系, 使用 extends 实现, 可以多继承.
- 接口和类 : 实现的关系, 使用 implements, 可以多实现.
- 类和类 : 继承的关系, 使用 extends 实现, 只能单继承, 但可以完成多重继承.
5.5 接口与抽象类的异同 :
- 抽象类是给所有的子类定义行为规则. 接口是给所有需要接口中行为的类定义统一的 `名称行为规范`.
- 抽象类可以定义抽象方法, 也可以非抽象方法. 接口中只能定义抽象方法.
- 抽象类中可以定义属性, 子类可以将属性参数在构造方法中传递给父类进行初始化.
- 接口中只有常量, 接口不属于类, 没有构造方法. public static final
- 抽象类中的定义功能为子类定义的, 子类可以继承父类中定义的行为.
- 接口是给一个类额外提供一些扩展功能. 这个扩展功能可以随着程序不断增强.
5.6 接口的好处 :
- 接口可以完成类与类之间的解耦. (程序的低耦合设计)
- 接口可以给一个类提供额外的扩展功能.
- 一方(Computer)使用规则, 另一方(Mouse,Keyboard,CameraVideo)实现规则, 此时, 双方就可以完成通信.
6. 面向接口编程 (重要)
耦合性概念 : 高耦合 / 低耦合
6.1 实现方式一 :
package cn.panda.demo3;
// 返厂 : 厂家拆电脑, 为电脑增加一个使用鼠标 / 键盘的功能.
public class Computer {
// 属性
// 行为 : 运行
public void run() {
System.out.println("Computer run ...");
}
// 修改源代码就类似于生活中的拆电脑
public void useMouse(Mouse m) {
if (m != null) {
m.open();
m.close();
}
}
public void useKeyboard(Keyboard k) {
if (k != null) {
k.open();
k.close();
}
}
}
package cn.panda.demo3;
public class Mouse {
// 属性
// 行为
public void open() {
System.out.println("Mouse open ...");
}
public void close() {
System.out.println("Mouse close ...");
}
}
package cn.panda.demo3;
public class Keyboard {
// 属性
// 行为
public void open() {
System.out.println("Keyboard open ...");
}
public void close() {
System.out.println("Keyboard close ...");
}
}
package cn.panda.demo3;
/*
* 需求 : 给电脑提供一个使用外接鼠标的功能.
* 需求 : 给电脑提供一个使用外接键盘的功能.
*
* 外接设备 : 扇热器, U盘, 手机, 耳机, 音响, 麦克风, 摄像头 ...
*/
public class TestComputer {
public static void main(String[] args) {
// 模拟买了一台电脑
Computer c = new Computer();
// 运行电脑
c.run();
// 模拟买了一个鼠标
Mouse m = new Mouse();
// 需求 : 电脑如何运行鼠标 ???
c.useMouse(m);
// 模拟买了一个键盘
Keyboard k = new Keyboard();
// 需求 : 电脑如何运行键盘 ???
c.useKeyboard(k);
}
}
6.2 实现方式二 :
笔记本是一个生产厂商. 鼠标是另外一个生产厂商.笔记本要使用外接的设备,笔记本上就要预留一些插口,而这些插口的预留大小和规则,一定要遵守某个规定,这时生产的笔记本才可以使用外接设备.而外接设备在生产的时候也应该遵守一定的规则,然后生产符合规则的设备.这样笔记本和外接设备之间就可以进行通信.
7. 静态与非静态的设计 (属性与方法)
- 静态 : 类 维度是类而不是对象
a) 调用方式 : 类名.属性名; 类名.方法名();
b) 属性 : 在内存中仅有一份. 因为一个类仅会被加载一次. 如果静态属性后期重新赋值, 结果就是新值覆盖旧值. ==类加载器,类被加载
c) 静态属性的说明 : 如果一个属性是共享的, 在内存中无需开辟多份, 这样的属性就应该被定义为静态属性. 比如(PI, SNAME …)
d) 静态方法中没有this. 在静态方法中无法获取成员属性的任何数据.
e) Cannot make a static reference to the non-static field 不能使静态引用指向一个非静态属性.
f) 方法中没有使用到成员属性的方法, 我们应该定义为静态方法. 使用 类名.方法名(); 不需要new对相关而直接实现调用. 这样可以节省内存空间.
- 非静态 : 对象
a) 调用方式 : 对象名.属性名; 对象名.方法名();
b) 属性 : 每一个对象都应该拥有自己独立的属性数据, 其它对象的数据应该与该对象毫无关系. (name, age, gender, id, score …)
c) 非静态方法中存在一个 `隐式` 的this. this 表示调用该方法的对象. 方法内部其实是通过 this 来获取对象的数据.
静态的,共享的,而非特例的.如果是属于类级别的(每个对象都具备的无差别属性),类中共享的,就不要再通过对象调用(对象的实例化会占据内存),直接通过类名调用和访问
静态成员即使通过对象访问,实则是通过对象所属的类在访问
静态修饰的成员,随着类的加载而加载(有初始化的默认值),方法区;与对象无关,与堆无关,先于对象存在(对象通过构造方法而存在)
Static修饰方法而言;
方法中没有用到实例(非静态)成员属性,就定义为static方法,使用类名直接调用;
静态和非静态的应用,如果方法里没有使用非静态成员变量,那就定义为静态方法
静态函数中不能使用非静态成员变量,
非静态函数中,应该具有非静态成员变量,具有super/this ,不然就该定义成静态函数
Static是修饰成员的,跟局部无关.
成员和局部
类中成员:成员变量,成员方法,构造方法,内部类,静态代码块/构造代码块, (主方法特殊,主方法只属于测试类)
局部:{}中(方法内部,代码块中,方法参数()中)
疑问:
为什么非静态的方法,没有用new对象去调用???
import org.junit.Test;
public class DemoTest {
@Test
public void test() throws Exception {
Object obj = returnObj();
System.out.println(obj);
}
public Object returnObj(){
return getSum(10,20);
}
public int getSum(int a,int b){
return a+b;
}
}
改成主方法: 主方法是静态的
public class DemoTest {
public static void main(String[] args) {
int returnObj = returnObj();
System.out.println(returnObj); //null
}
public int returnObj() {
return getSum(10, 20);
}
public int getSum(int a,int b){
System.out.println("huahuashijie");
return a+b;
}
}
public class DemoTest {
public static void main(String[] args) {
int returnObj = returnObj();
System.out.println(returnObj); //null
}
public static int returnObj(){
return getSum(10, 20);
}
public int getSum(int a,int b){
System.out.println("huahuashijie");
return a+b;
}
}
再改
public class DemoTest {
public static void main(String[] args) {
int returnObj = returnObj();
System.out.println(returnObj); //null
}
public static int returnObj(){
DemoTest demoTest = new DemoTest();
return demoTest.getSum(10, 20);
}
public int getSum(int a,int b){
//将方法改成static 或者 调用此方法的通过对象调用
System.out.println("huahuashijie");
return a+b;
}
}
package cn.panda.demo5;
public class Student {
// 属性 (name, age, gender ...)
private String name;
private int age;
private char gender;
// 静态属性 (public static final) 公开的, 静态的, 不可修改的.
public static final double PI = 3.14;
// 构造方法
public Student(String name, int age, char gender) {
super();
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
// 说明 : 只要在方法内部没有使用到 `成员 / 对象属性`, 那么该方法就应该被定义为 `静态方法`.
// 求 `圆` 的面积 公式 : PI * r * r
public static double getArea(double r) {
return PI * r * r;
}
// 行为 :
// 介绍自己 : introduce
public void introduce() {
// 说明 : 只要在方法内部使用到 `成员 / 对象属性`, 那么该方法就一定要定义为 `非静态方法`.
// this 是谁 ??? 当前调用该方法的对象.
System.out.println("大家好, 我叫" + name + ",我今年" + age + "岁了. " + gender);
// System.out.println("大家好, 我叫" + this.name + ",我今年" + this.age + "岁了. " + this.gender);
}
// setter & getter
}
package cn.panda.demo5;
public class TestStudent {
public static void main(String[] args) {
Student stu = new Student("张三", 18, '男');
Student stu2 = new Student("丽丽", 16, '女');
Student stu3 = new Student("李四", 18, '男');
// String name = stu.getName();
// String name2 = stu2.getName();
// System.out.println(name + " : " + name2);
stu.introduce();
stu2.introduce();
stu3.introduce();
}
}
package cn.panda.demo5;
public class TestMethod {
public static void main(String[] args) {
double area = Student.getArea(2.5);
System.out.println(area);
/*
Student stu = new Student("渣渣辉", 18, '男');
// The static method getArea(double) from the type Student should be accessed in a static way
// 静态方法应该使用 静态方式(类的方式) 实现访问.
double area = stu.getArea(2.5);
System.out.println(area);
*/
}
}
8. 访问权限修饰符 (了解)
private < 默认 < protected < public
private 私有化权限修饰符 :
如果有子类,子类虽然继承,但也没权限使用,属性要通过get方法,方法则不能重写
package cn.panda.demo2;
public class Animal {
// 属性
private String brand;
// 行为 :
public void showBrand() {
brand = "高级品种";
System.out.println("Animal brand = " + brand);
}
// 间接访问 :
public String getBrand() {
return brand;
}
}
package cn.panda.demo2;
public class Dog extends Animal {
// 属性
// 行为
public void show() {
// The field Animal.brand is not visible 不可见.
// System.out.println("查看父类中继承而来的 brand : " + brand); // 错误! 私有化属性在其他中不能直接访问.
System.out.println("查看父类中继承而来的 brand : " + getBrand()); // 间接访问
}
}
包级别访问权限 :
package cn.panda.demo2;
public class Animal {
// 属性
private String brand;
String name; 只有本包的类能直接调用,其他包不能使用
// 行为 :
public void showBrand() {
brand = "高级品种";
System.out.println("Animal brand = " + brand);
System.out.println("Animal name = " + name);
}
// 间接访问 :
public String getBrand() {
return brand;
}
}
package cn.panda.demo2;
public class Dog extends Animal {
// 属性
// 行为
public void show() {
// The field Animal.brand is not visible 不可见.
// System.out.println("查看父类中继承而来的 brand : " + brand); // 错误! 私有化属性在其他中不能直接访问.
System.out.println("查看父类中继承而来的 brand : " + getBrand()); // 间接访问
System.out.println("查看父类中继承而来的 name : " + name); // 直接访问
}
}
package cn.panda.demo1;
import cn.panda.demo2.Animal;
public class Cat extends Animal {
// 属性
// 行为 :
public void show() {
// System.out.println("访问 Animal 类中的 name 属性 : " + name); 错误!
}
}
子类访问级别权限 : protected
package cn.panda.demo2;
public class Animal {
// 属性
private String brand;
String name;
protected int age;
// 行为 :
public void showBrand() {
brand = "高级品种";
System.out.println("Animal brand = " + brand);
System.out.println("Animal name = " + name);
System.out.println("Animal age = " + age);
}
// 间接访问 :
public String getBrand() {
return brand;
}
}
package cn.panda.demo2;
public class Dog extends Animal {
// 属性
// 行为
public void show() {
// The field Animal.brand is not visible 不可见.
// System.out.println("查看父类中继承而来的 brand : " + brand); // 错误! 私有化属性在其他中不能直接访问.
System.out.println("查看父类中继承而来的 brand : " + getBrand()); // 间接访问
System.out.println("查看父类中继承而来的 name : " + name); // 直接访问
System.out.println("查看父类中继承而来的 age : " + age); // 直接访问
}
}
package cn.panda.demo1;
import cn.panda.demo2.Animal;
public class Cat extends Animal {
// 属性
// 行为 :
public void show() {
// System.out.println("访问 Animal 类中的 name 属性 : " + name); 错误!
System.out.println("访问 Animal 类中的 age 属性 : " + age);
}
}
说明 : 两个类中都访问了 Animal 类中定义的 protected 修饰的 age 属性. 不同包中的类错误!
protected 修饰的属性, 如果使用类与 Animal 不是子父类关系, 也不再同一个包中, 因此就不可以使用. (了解)
比default权限要大,同一个包肯定可以访问,即使不在同一个包,如果是父子类关系,也可以访问
公共访问权限 : public (任何包, 任何类都可以直接访问)
package cn.panda.demo2;
public class Animal {
// 属性
private String brand;
String name;
protected int age;
public char gender;
// 行为 :
public void showBrand() {
brand = "高级品种";
System.out.println("Animal brand = " + brand);
System.out.println("Animal name = " + name);
System.out.println("Animal age = " + age);
System.out.println("Animal gender = " + gender);
}
// 间接访问 :
public String getBrand() {
return brand;
}
}
package cn.panda.demo2;
// TestAccess 与 Animal 没有任何关系 ??? 该类能否访问 Animal 中的 protected 修饰的属性 ??? 同一个包中可以, 不同包中错误!
public class TestAccess {
public static void main(String[] args) {
Animal a = new Animal();
a.gender = '雌';
a.age = 100;
}
}
9. 内部类 :
定义一个类 :
- 类名 : 见名知意
- 属性 : 存储数据 (name, age, gender, id, score …)
- 行为 : 操作数据. (introduce, getArea …)
一个类中不仅可以定义 `属性和行为`, 还可以定义一个 `类`. 这个类就会被称为 `内部类`. 所属的类被称为 `外部类`.
(了解 : Java源代码中会使用)
- 成员内部类. 定义在成员属性位置上. 内部不可以定义 `静态属性和静态方法
- 静态内部类. 定义在成员属性位置上. 拥有 static 关键字. 内部可以定义 `静态属性和静态方法`
- 方法内部类. 定义在方法内部. 只能在方法结束之前被使用.
内部类生成的 Class 文件 : $ 该符号表示所属关系.
成员内部类 :
package cn.panda.demo3;
// 类的作用 : 将对象相关的数据和行为封装到同一个类中.
// 定义了一个 `外部类`
public class OutterClass1 {
// 属性
private String name;
private int age;
// 定义一个成员 `内部类`
// 内部类也是用来封装数据和行为的, 那么为什么要将一个类定义在内部呢 ???
// 解答 : 为了不让别的类使用,只让自己本类来使用.
// 注意点 : 在成员(对象)内部类中不可以使用 `静态(类)`.
// 加载时间 : 成员内部类加载的时间在该类创建对象的时候.
private class InnerClass1 {
// 属性
private String id;
// public static String schoolName = "上海传智播客";
// 行为
public void show() {
System.out.println("id = " + id);
}
/*public static void show2() {
System.out.println("");
}*/
}
// 行为
public void introduce() {
System.out.println("name = " + name);
System.out.println("age = " + age);
}
}
静态内部类 :
package cn.panda.demo3;
// 类的作用 : 将对象相关的数据和行为封装到同一个类中.
// 定义了一个 `外部类`
public class OutterClass2 {
// 属性
private String name;
private int age;
// 定义一个静态 `内部类`
// 特点 : 静态内部类是跟随 `外部类同时加载`. 因此, 内部可以定义静态属性和方法.
private static class InnerClass2 {
// 属性
private String id;
public static String schoolName = "上海传智播客";
// 行为
public void show() {
System.out.println("id = " + id);
}
public static void show2() {
System.out.println("静态方法...");
}
}
// 行为
public void introduce() {
System.out.println("name = " + name);
System.out.println("age = " + age);
}
}
方法内部类 :
package cn.panda.demo3;
// 定义了一个 `外部类`
public class OutterClass3 {
// 属性
private String name;
private int age;
// 行为
// 说明 : 方法只有被调用时, 入栈后才可以执行.
public void introduce() {
System.out.println("name = " + name);
System.out.println("age = " + age);
// 定义了一个方法内部类 :
class InnerClass3 {
String name;
int age;
public void show() {
System.out.println("InnerClass3 类的show 方法 : name = " + name + ", age = " + age);
}
}
// 在本方法结束之前, 一定要使用.
InnerClass3 in = new InnerClass3();
in.name = "Jack";
in.age = 18;
in.show();
}
}
9.1 综合案例一 : 电脑与USB接口
情况一 : new 接口() { 实现接口中定义的抽象方法 };
重要 : 匿名内部类 (Java 8, 9)
Mouse, Keyboard, CameraVideo 外部类.
package cn.panda.demo4;
public class Computer {
// 属性
// 行为 :
public void run() {
System.out.println("Computer run ...");
}
// 面向接口编程 :
public void useUSB(USB usb) {
if (usb != null) {
usb.open();
usb.close();
}
}
}
package cn.panda.demo4;
public interface USB {
void open();
void close();
}
package cn.panda.demo4;
// Mouse 是一个外部类 : 可以被本项目其它类共同使用.
// 外部类的好处 : 1. 可以被看见. 2. 可以被其它类使用.
// 请问 : 需要 10 个 Mouse 对象. new 十个 Mouse 对象就可以了.
public class Mouse implements USB {
@Override
public void open() {
System.out.println("Mouse open ...");
}
@Override
public void close() {
System.out.println("Mouse close ...");
}
}
测试类一 :
package cn.panda.demo4;
public class TestComputer {
public static void main(String[] args) {
Computer c = new Computer();
c.run();
Mouse m = new Mouse();
c.useUSB(m);
// 方法内部类 : (了解)
class Keyboard implements USB {
@Override
public void open() {
System.out.println("Keyboard open ...");
}
@Override
public void close() {
System.out.println("Keyboard close ...");
}
}
// 使用 Keyboard
Keyboard k = new Keyboard();
c.useUSB(k);
// 匿名内部类对象 :
/*
* 语法 :
* new USB(); 错误! USB 是接口, 接口不可以实例化. 接口甚至都构造方法.
* 它没有 USB 接口中抽象方法的实现体.
*
* {} 大括号表示的就是 `实现体`.
*
* Java中的特殊语法 : 接口只要拥有一个实现体. 那么它就是该接口的 `实现类` 对象.
*
* new USB(){}; 正确! 创建了一个 USB 接口类型的 `匿名` 实现类对象. 在方法体中实现接口中定义的抽象方法即可.
*/
// Cannot instantiate the type USB 不能实例化 USB 接口.
// USB usb = new USB();
// 请问1 : useUSB 该方法参数类型是什么 ??? USB 接口类型. 其实真正需要的就是 USB 接口的实现类对象.
// 请问2 : 之前的 m (Mouse类型) 和 k (Keyboard类型) 的对象都可以传入, 原因是什么 ??? 是因为 m, k 都是 USB 接口的实现类.
// 请问3 : 只要是 USB 接口的实现类对象, 就可以作为参数传入到该方法吗 ??? 是的.
c.useUSB(new USB(){
@Override
public void open() {
System.out.println("Micphone open ...");
}
@Override
public void close() {
System.out.println("Micphone close ...");
}
});
类作为参数传入,传入的是类的实例化对象,可以写成匿名对象
/*
Student stu = new Student();
方法(stu);
方法(new Student());
*/
}
}
测试二 :
package cn.panda.demo4;
public class TestComputer2 {
public static void main(String[] args) {
Computer c = new Computer();
c.run();
// 面向接口编程 : 接口不可能有对象.
// 如果一个参数是接口类型,其真正需要的是该接口的实现类对象.
// USB usb = new Mouse();
// 数量 : 如果需要 10 个匿名实现体, 那么就需要完成 10 次实现体书写.
// 匿名实现类的使用场景 : 在当前类中仅需要被使用一次.(匿名的,直接就是一个实现体,没有名称可以接收,无法多次使用)
// 多态语句接收接口实现类对象.
USB usb = new USB(){
@Override
public void open() {
System.out.println("Micphone open ...");
}
@Override
public void close() {
System.out.println("Micphone close ...");
}
};
c.useUSB(usb);
/*
USB usb2 = new USB(){
@Override
public void open() {
System.out.println("Mouse open ...");
}
@Override
public void close() {
System.out.println("Mouse close ...");
}
};
USB usb3 = new USB(){
@Override
public void open() {
System.out.println("Keyboard open ...");
}
@Override
public void close() {
System.out.println("Keyboard close ...");
}
};
*/
// c.useUSB(usb2);
// c.useUSB(usb3);
}
}
9.2 综合案例二 : Animal与抽象shout()方法
情况二 : new 抽象类() { 实现抽象类中定义的抽象方法 };
Animal 抽象类 : shout() 抽象方法.
Dog, Cat, Wolf 外部类.
package cn.panda.demo5;
public abstract class Animal {
// 属性
private String name;
// 行为
public abstract void shout();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package cn.panda.demo5;
// Dog 是一个外部类
public class Dog extends Animal {
@Override
public void shout() {
System.out.println(getName() + " : 汪汪汪");
}
}
测试类 :
package cn.panda.demo5;
public class TestAnimal {
public static void main(String[] args) {
// 定义一个类 : (方法内部类)
class Cat extends Animal {
@Override
public void shout() {
System.out.println(getName() + " : 喵喵喵");
}
}
// Cannot instantiate the type Animal
// Animal a = new Animal();
// 需求 : 需要一个 Animal 类型的子类对象.
// Animal a = new Dog();
// Animal a = new Cat();
/*
* 说明 : Dog 和 Cat 都是 Animal 类的子类, 因此可以作为 animalSound 该方法的参数传入.
*
* 说明 : 只要给抽象类增加一个方法体, 此时创建的就不再是 `抽象类对象`.
*/
// 多态的语法 : 创建了一个 Animal 类的匿名子类对象.
Animal a = new Animal() {
@Override
public void shout() {
System.out.println(getName() + " : 亚麻得...");
}
};
// 设置属性
a.setName("色狼");
// 传递匿名子类对象
animalSound(a);
// 直接传入
animalSound(new Animal() {
@Override
public void shout() {
String name = getName();
name = "打色狼";
System.out.println(name + " : 亚麻得...");
}
});
// 创建的是谁呀 ??? Dog 类的匿名子类. 傻狗/洋狗/土狗等等等
Dog dog = new Dog() {
public void run() {
System.out.println("子类在裸奔...");
}
};
}
// 行为 : 动物之声
public static void animalSound(Animal a) {
a.shout();
}
}
9.3 关于匿名内部类的面试题 : (了解)
package cn.panda.demo3;
public class InterviewTest {
public static void main(String[] args) {
// 多态语法 : 使用 obj 对象调用方法, 编译器都会去 Object 类中查找
// Object obj = new Object(){}; // Object 类的匿名子类
Object obj = new Object() {
public void show() {
System.out.println("我是 show() 方法...");
}
};
匿名子类/实现类自己的方法,无法被多态形式表示的父类通过变量名调用,(多态中父类无法调用父类不具有的方法)
// The method show() is undefined for the type Object Object类中找不到 show()方法.
// 无法调用show()方法, 匿名子类没有名称, 无法实现向下转型.
// 子类类型 子类名 = (子类类型) obj;
// obj.show();
System.out.println("------------------------");
new Object() {
public void show() {
System.out.println("我是 show() 方法...");
}
}.show();
}
}
10. Object 类 (祖宗类)
创建一个类, 如果这个类没有 `显示` 继承任何一个类, 那么其实该默认继承 Object 类.
Person Person 类直接继承了 Object 类.
Student extends Person Student 类间接继承了 Object 类.
Object 是顶层的 `共性类`. 该类中定义的所有 `属性和行为` 默认任何一个子类都会拥有.
定义了一个Person类,然后直接创建了2个Person对象,如果Person类两个对象的的姓名和年龄都相同,我们就认为是同一个人,调用equals应该返回true.
10.1 equals() 方法详解
Object 类内部的 equals 方法实现 == 符号判断两个对象是否相等.
- Object 类无法确定类的判断规则. 怎么样才算是判断两个类相等
- 因此Object 类认为如果两个对象的内存地址相等, 那么这两个对象就相等.
地址相等,一定相同,但是地址不等,也可能是相同的
Object 类内部的 this 就是 p1 对象.
Object 类内部的 obj 就是 p2 对象.
结论 : Object 类内部 equals 方法的实现不是我们需要的. 怎么办 ???
解决方案 : Person 类重写 Object 类的 equals 方法. 此时, 程序运行时, 就会执行子类重写的方法.
String 类重写了 Object 的 equals 方法.实现了根据字符数据来判断.
Person 类重写的代码实现 :
Object 类中 equals 方法, 几乎所有的子类都会进行重写.
重写 Object 类的 equals 方法的目的是什么 ??? 实现子类自定义比较的规则.
10.2 toString() 方法详解
想要什么 ??? Person[name = 张三, age = 18]
解决方案 : Person 类重写 Object 类的 toString() 方法.
Person 类重写 Object 类 toString() 方法的目的 ??? 自定义本类对象的输出信息.
全部代码 :
package cn.panda.demo6;
public class Person {
// 属性
private String name;
private int age;
// 行为
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Person() {
}
// 重写 Object 类的 toString() 方法
@Override
public String toString() {
return "Person[name = "+name+", age = "+age+"]";
}
// 重写 Object 类的 equals 方法
@Override
public boolean equals(Object obj) {
// System.out.println("this = " + this);
// System.out.println("obj = " + obj);
// 1. 判断两个对象的内存地址是否相等
if (this == obj) {
return true;
}
// 2. 如果两个对象的内存地址不相等, 怎么办 ??? 再判断两个对象的 `数据` 是否相等.
if (obj instanceof Person) {
// 3. 条件如果成立, 说明 obj 就是 Person 类型的对象, 需要强转
Person p = (Person) obj;
// 4. 比较两个对象的 name 和 age
if (this.name.equals(p.name) && this.age == p.age) {
// 5. 如果条件成立, 说明两个对象的 name 和 age 完全相等.
return true;
}
}
// 6. 如果 obj 不是 Person 对象, 或者两个对象的数据不相等, 都返回false
return false;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package cn.panda.demo6;
public class TestObjectMethod {
public static void main(String[] args) {
// 需求 : 定义了一个Person类,然后直接创建了2个Person对象,
// 如果Person类两个对象的的姓名和年龄都相同,我们就认为是同一个人.
Person p1 = new Person("张三", 18);
Person p2 = new Person("李四", 18);
// Java 语言规则 : 直接输出对象, 就等于调用该对象的 toString() 方法.
// 输出对象的目的就是为了查看对象中的数据. (调试程序, 查看数据)
System.out.println("p1 = " + p1.toString());
System.out.println("p2 = " + p2.toString());
// 判断 :
// 祖宗类给所有子类定义一个判断对象是否相等的规则 : equals
// boolean result = p1 == p2;
// Object 类的 equals 方法的实现不是我们需要的 ???
boolean result = p1.equals(p2);
System.out.println(result);
// test();
}
public static void test() {
int num1 = 10;
int num2 = 10;
boolean result = num1 == num2;
System.out.println(result);
}
}
11. 异常类 Exception :
请问为什么Java语言要设计异常类 ??? 提醒机制
说明 : 抛出异常 throw 关键字实现.
请问 : throw 关键字之后可以书写哪些对象呢 ??? 异常对象.
哪些对象是可以 throw 的 ??? 只要是 Throwable 类下的所有子类都可以被 throw 出去.
Error 错误 : 内存溢出错误. 类找不到错误. (很严重的问题.)
Exception 异常 : (提醒机制)
注意 : 除了 RuntimeException 类, 其余的全部被称为 `编译时` 异常.
RuntimeException 及其子类全部被称为 `运行时` 异常, 特点是这类的异常编译器不会做任何检查.
Unchecked Exception 不检查异常 编译器不检查. 无法代码是否正确, 都不错误! RuntimeException 和子类
Checked Exception 检查异常 编译器一旦发现有该类异常, 无法代码是否正确, 立即报错.
package cn.panda.demo1;
public class TestException {
public static void main(String[] args) {
// 编写程序 : 希望程序一切正常.
// 1. NullPointerException 空引用 / 空指针异常
// 2. ArrayIndexOutOfBoundsException 数组下标越界异常
int[] arr = {10, 20, 30};
// 调用方法
int result = getElement(arr, 3);
System.out.println(result);
System.out.println("程序执行完成...");
}
// 定义一个方法, 根据下标获取指定元素的数值, 并返回给调用者
public static int getElement(int[] arr, int index){
int element = arr[index];
return element;
}
}
11.1 如何手动抛出异常: throw new 异常对象();
package cn.panda.demo1;
public class TestException {
public static void main(String[] args) {
// 编写程序 : 希望程序一切正常.
int[] arr = {10, 20, 30};
// 调用方法
int result = getElement(null, 3);
System.out.println(result);
System.out.println("程序执行完成...");
}
// 定义一个方法, 根据下标获取指定元素的数值, 并返回给调用者
public static int getElement(int[] arr, int index){
// 1. NullPointerException 空引用 / 空指针异常
if (arr == null) {
// 格式 : throw 异常对象(信息);
throw new NullPointerException("数组引用不能为空. 请修正.");
}
// 2. ArrayIndexOutOfBoundsException 数组下标越界异常
if (index < 0 || index >= arr.length) {
throw new ArrayIndexOutOfBoundsException("数组下标" + index + "错误! 请修正.");
}
int element = arr[index];
return element;
}
}
11.2 运行时异常的处理方案1 : 不处理
说明 : 因此编译器都不检查, 因此可以选择不处理.
11.3 运行时异常的处理方案2 : 捕获
格式 : try – catch – catch
try {
可能会发生异常的代码 …
} catch (异常类型 异常名) {
对发生的异常进行处理… 说明: 异常处理完毕后, 程序会继续向后执行, 不会返回到异常发生的位置.
}
package cn.panda.demo1;
public class TestException2 {
public static void main(String[] args) {
// 编写程序 : 希望程序一切正常.
int[] arr = {10, 20, 30};
// 调用方法
try {
int result = getElement(null, 3);
System.out.println(result);
} catch (NullPointerException e) {
System.out.println("程序发生了空引用异常...");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("程序发生了数组下标越界异常...");
}
int num1 = 10;
int num2 = 20;
int sum = num1 + num2;
System.out.println("程序执行完成... sum = " + sum);
}
// 定义一个方法, 根据下标获取指定元素的数值, 并返回给调用者
public static int getElement(int[] arr, int index){
int element = arr[index];
return element;
}
}
请问 : 一个 catch 能捕获两个不同类型的异常吗 ??? 可以, 使用多态语法. 程序可以正常执行结束.
package cn.panda.demo1;
public class TestException3 {
public static void main(String[] args) {
// 编写程序 : 希望程序一切正常.
int[] arr = {10, 20, 30};
// 调用方法
try {
int result = getElement(arr, 3);
System.out.println(result);
} catch (RuntimeException e) {
// String message = e.getMessage(); // null
// String str = e.toString(); // java.lang.NullPointerException
e.printStackTrace();
}
int num1 = 10;
int num2 = 20;
int sum = num1 + num2;
System.out.println("程序执行完成... sum = " + sum);
}
// 定义一个方法, 根据下标获取指定元素的数值, 并返回给调用者
public static int getElement(int[] arr, int index){
int element = arr[index];
return element;
}
}
请问 : 如果 catch 块没有捕获到发生异常的类型, 程序会怎么样 ??? 能正常执行结束吗 ???
说明 : catch 如果没有捕获到, 等于没有捕获.
package cn.panda.demo1;
public class TestException3 {
public static void main(String[] args) {
// 编写程序 : 希望程序一切正常.
int[] arr = {10, 20, 30};
// 调用方法
try {
int result = getElement(arr, 3);
System.out.println(result);
} catch (NullPointerException e) {
// String message = e.getMessage(); // null
// String str = e.toString(); // 异常类型
e.printStackTrace();
}
int num1 = 10;
int num2 = 20;
int sum = num1 + num2;
System.out.println("程序执行完成... sum = " + sum);
}
// 定义一个方法, 根据下标获取指定元素的数值, 并返回给调用者
public static int getElement(int[] arr, int index){
int element = arr[index];
return element;
}
}
11.4 编译时异常的处理方案1 : 声明
格式 : throws 异常类型
位置 : 在方法参数列表之后.
package cn.panda.demo2;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TestIO {
public static void main(String[] args) throws IOException {
// 调用方法
readFile("demo1.txt");
}
// 定义一方法, 读取文件中的数据, 返回读取结果
public static String readFile(String fileName) throws IOException {
// 1. 创建一个高效的缓冲字符输入流
// Unhandled exception type FileNotFoundException 未处理的异常类型.
BufferedReader reader = new BufferedReader(new FileReader(fileName));
// 2. 一行一行读
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 3. 关闭资源
reader.close();
// 4. 返回读取结果
return "读取成功!";
}
}
结果一 : 一切正常, 程序正常执行完毕…
结果二 : 如果文件不存在, 程序异常结束, 提供终止了程序.
11.5 编译时异常的处理方案2 : 捕获
格式 : try – catch - finally
try {
编程可能会发生异常的代码 …
} catch (异常类型 异常名) {
处理异常发生后的业务逻辑 …
} finally {
关闭开辟资源… (无论是否发生异常, finally块中的代码都会被执行.)
}
package cn.panda.demo2;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TestIO2 {
public static void main(String[] args) {
// 调用方法
String result = readFile("demo1.txt");
System.out.println("正常执行完毕 -> " + result);
}
// 定义一方法, 读取文件中的数据, 返回读取结果
public static String readFile(String fileName) {
BufferedReader reader = null; // 定义
try {
// 1. 创建一个高效的缓冲字符输入流
reader = new BufferedReader(new FileReader(fileName)); // 初始化
// 2. 一行一行读
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
return "文件读取失败";
} finally {
// 3. 关闭资源
System.out.println("执行finally中的代码 ...");
if (reader != null) {
try {
reader.close(); // NullPointerException
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 4. 返回读取结果
return "读取成功!";
}
}
11.6 异常的细节 :
注意点 : Override
- 如果子类重写父类的方法, 父类该方法没有声明编译时异常, 子类重写时也不可以声明编译时异常.
- 如果一个类实现接口中的抽象方法, 如果该抽象方法没有声明编译时异常, 接口实现时也不可以声明编译时异常.
package cn.panda.demo3;
public class Person {
// 属性
// 行为 :
public void introduce() {
System.out.println("大家好, 我是Person类.");
}
}
package cn.panda.demo3;
public interface MyInterface {
void run(); // 运行
}
package cn.panda.demo3;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Student extends Person implements MyInterface {
@Override
// Exception IOException is not compatible with throws clause in Person.introduce()
// IOException 异常和父类中 introduce() 方法的声明语句不兼容.
public void introduce() {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("demo1.txt"));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// ignore 忽略
e.printStackTrace();
}
}
}
}
@Override
public void run() {
// new BufferedReader(new FileReader("demo1.txt"));
}
}
11.7 异常思考题
package cn.panda.demo3;
public class ThinkingInException {
public static void main(String[] args) {
System.out.println("1");
try {
System.out.println("2");
int num = 10 / 2; // ArithmeticException
System.out.println("3");
String s1 = "";
System.out.println(s1.toString()); // 没有问题
System.out.println("4");
String s2 = "";
System.out.println(s2.toString()); // NullPointerException
System.out.println("5");
int[] arr = {10, 20, 30};
System.out.println(arr[2]); // ArrayIndexOutOfBoundsException
System.out.println("6");
// 多态 :
Object obj = new Student();
Student stu = (Student) obj; // ClassCastException
System.out.println("7");
} catch (ArithmeticException e) {
System.out.println("8");
} catch (NullPointerException e) {
System.out.println("9");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("10");
} catch (ClassCastException e) {
System.out.println("11");
} finally {
System.out.println("12");
}
System.out.println("13");
}
}
12. 正则表达式 : (String类)
Regex -> Regular Expression 正则表达式
正则表达式其实就是在Java语言中的一些特殊符号, 这些符号写在对应的参数 (Regex) 位置上.
- [] 表示取值的范围.
- {} 表示前一个值可以出现的次数. {4,11} 4到11次. {4,} 4到无穷次 {4} 只能出现4次
a) 数量词 :
b) ? 0次或1次 {0,1}
c) + 1次或更多 {1,}
d) * 0次或更多 {0,}
- () 表示分组. 分组的编号自动从1开始. 在replecement参数方法中, 使用 $ 符号来引用之前定义的组.
12.1、案例引入 – matches (匹配)
需求 : 验证QQ号码是否合法.
分析 :
- 第一位不能是 0 [1-9]//d{4,11}
- QQ号码在 5 ~ 12 位之间 (包含)
- QQ号码都是由数字组成.
package cn.panda.demo4;
public class TestVerifyQQNumber {
public static void main(String[] args) {
boolean result = verifyQQNumber3("1234567891234");
System.out.println(result);
}
// 定义一个方法 : 网页文本框中获取都是 `字符串数据`.
public static boolean verifyQQNumber(String qq) {
/*
需求 : 验证QQ号码是否合法.
分析 :
1. 第一位不能是 0
2. QQ号码在 5 ~ 12 位之间 (包含)
3. QQ号码都是由数字组成.
*/
if (qq.charAt(0) == '0') {
return false;
} else if (qq.length() < 5 || qq.length() > 12) {
return false;
} else {
// 遍历
for (int i = 0; i < qq.length(); i++) {
char ch = qq.charAt(i);
if (ch < '0' || ch > '9') {
return false;
}
}
}
return true;
}
public static boolean verifyQQNumber2(String qq) {
/*
需求 : 验证QQ号码是否合法.
分析 :
1. 第一位不能是 0
2. QQ号码在 5 ~ 12 位之间 (包含)
3. QQ号码都是由数字组成.
*/
if (qq.charAt(0) == '0') {
return false;
} else if (qq.length() < 5 || qq.length() > 12) {
return false;
} else {
// 判断 : Long 包装类 -> long parseLong(字符串); 尝试将一个字符串转换为 long 基本数据类型
// 运行时异常有两种处理方案 : 1. 不处理 (这里不符合业务需要) 2. 捕获
try {
Long.parseLong(qq);
} catch (NumberFormatException e) {
return false;
}
}
return true;
}
public static boolean verifyQQNumber3(String qq) {
// 正则 : 底层还是 Java 代码, 编译器可以根据正则的规则将字符串翻译成 Java 代码.
return qq.matches("[1-9][0-9]{4,11}");
}
}
12.2、切割 – split (字符串)
package cn.panda.demo4;
public class TestSplit {
public static void main(String[] args) {
// 切割
// String str = "boxing#######basketball#####football###ILOVEYOU###爱我中华";
// String[] names = str.split("#+"); // {1,}
// String str = "boxing123basketball4567football888ILOVEYOU9876爱我中华";
// String[] names = str.split("[0-9]+"); // {1,}
// String[] names = str.split("\\d+"); // {1,}
String str = "boxing basketball football ILOVEYOU 爱我中华";
String[] names = str.split(" +");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
}
}
12.3、替换 – replaceAll (隐藏手机号码)
需求:验证手机号码 :
- 手机号码规则 :
- 1. 长度必须是11位
- 2. 第一位只能是数字1
- 3. 第二位可以是3, 4, 5, 7, 8
- 4. 从第三位开始可以是 0-9
号码 : 13366285946 -> 133****5946
package cn.panda.demo4;
public class TestTelphone {
public static void main(String[] args) {
/*
手机号码规则 : 规则的匹配 (String类的 matches方法)
1. 长度必须是11位
2. 第一位只能是数字1
3. 第二位可以是3, 4, 5, 7, 8
4. 从第三位开始可以是 0-9
*/
String tel = "18866286868";
boolean result = tel.matches("[1][34578][0-9]{9}");
System.out.println(result);
// 隐藏 : (String类replaceAll方法)
// String replaceAll(String regex, String replacement);
// 使用 replacement 字符串内容替换匹配到的所有数据内容
// 需要给规则分组 : () 在replacement参数中使用 $ 符号引用前面规则的组
// 133 (1[34578]\\d) 第1组
// 6628 ([0-9]{4}) 第2组
// 5946 ([0-9]{4}) 第3组
String str = tel.replaceAll("(1[34578]\\d)([0-9]{4})([0-9]{4})", "$1****$3");
System.out.println(str);
}
}
13. 常用类介绍
1. System 系统类
作用 : 测试程序某段代码的执行效率.
// out 表示标准输出流 (控制台)
System.out.println();
// 退出Java虚拟机
System.exit(0);
package cn.panda.demo1;
public class TestSystem {
public static void main(String[] args) {
// 天河二号 -> 太湖.神威之光 (超算) 12.5亿亿次
// 记录开始时间
// 时间毫秒 (从1970年1月1日 00:00:00 开始到刚才过了多少毫秒数)
long start = System.currentTimeMillis();
long sum = 0;
for (int i = 0; i < 1000000000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("共耗时 : " + (end - start) + "毫秒."); // 共耗时 : 394毫秒.
}
}
2. Math 数学类
1. abs 绝对值
2. ceil, floor, round 向上取整 / 向下取整 / 四舍五入
3. max, min 最大值 / 最小值 (三元运算符)
4. pow 次幂 random 随机数 (0.0 ~ 1.0 之间)
package cn.panda.demo1;
public class TestMath {
public static void main(String[] args) {
// 1. abs 绝对值
System.out.println(Math.abs(-88));
System.out.println(Math.abs(88));
// 2. ceil, floor, round 向上取整 / 向下取整 / 四舍五入
System.out.println(Math.ceil(8.01)); // 9.0
System.out.println(Math.floor(8.99)); // 8.0
System.out.println(Math.round(8.5)); // 9
// 3. max, min 最大值 / 最小值 (三元运算符) 只能比较两个数
System.out.println(Math.max(10, 20));
System.out.println(Math.min(10, 20));
// 4. pow 次幂 random 随机数 (0.0 ~ 1.0 之间)
System.out.println(Math.pow(10, 3)); // 10 * 10 * 10
System.out.println((int)(Math.random() * 100));
}
}
3. Random 随机类
Random 是一个 `伪随机` 类. (seed 种子)
如果用相同的种子创建两个 Random
实例,则对每个实例进行相同的方法调用序列,它们将生成并返回相同的数字序列.
package cn.panda.demo1;
import java.util.Random;
public class TestRandom {
public static void main(String[] args) {
// 创建一个随机数对象
Random r = new Random(); // 底层也有种子, 种子就是当前时间毫秒.
int num = r.nextInt(101); // 0 ~ 100
System.out.println(num);
// 传递一个种子
Random r2 = new Random(998);
int num2 = r2.nextInt(101);
System.out.println(num2);
Random r3 = new Random(998);
int num3 = r3.nextInt(101);
System.out.println(num3);
}
}
4. Arrays 数组工具类
1. sort 排序.
2. toString(); 将数组中的元素以字符串形式来进行实现.
package cn.panda.demo1;
import java.util.Arrays;
public class TestArrays {
public static void main(String[] args) {
int[] numbers = {88, 56, 66, 78, 35, 87, 99, 59};
System.out.println(numbers); // [I@15db9742
// [88, 56, 66, 78, 35, 87, 99, 59]
String str = Arrays.toString(numbers);
System.out.println(str);
// 排序 : 从小到大
Arrays.sort(numbers);
str = Arrays.toString(numbers);
System.out.println(str);
System.out.println("-----------------");
String s = "xyzopqtsvabcgef";
// 将一个字符串转换为字符数组
char[] charArray = s.toCharArray();
// 对字符数组实现排序
Arrays.sort(charArray);
str = Arrays.toString(charArray);
System.out.println(str);
}
}
14. 日期类介绍
1. Date 日期类
Date 类中大部分方法都已经过时.
需求 : 获取用来表示当前时间的日期对象. new Date();
package cn.panda.demo2;
import java.util.Date;
public class TestDate {
public static void main(String[] args) {
// 获取当前时间对象
Date date = new Date();
System.out.println(date);
// 获取时间毫秒
long time = date.getTime();
System.out.println(time);
long time2 = System.currentTimeMillis();
System.out.println(time2);
// Date(long time); 从标准时间开始计算
Date date2 = new Date(1000 * 60 * 60 * 24);
System.out.println(date2);
}
}
2. DateFormat 日期格式化
作用 :
1. String format(Date对象); 将一个日期对象转换为字符串形式.
2. Date parse(String对象); 将一个字符串解析一个日期对象.
DateFormat 是日期/时间格式化子类的抽象类,它以与语言无关的方式格式化并解析日期或时间。
日期/时间格式化子类(如 SimpleDateFormat)允许进行格式化(也就是日期 -> 文本)、解析(文本-> 日期)和标准化
import java.util.Date;
public class TestDateFormat {
public static void main(String[] args) {
method_1(new Date());
System.out.println("---------------");
String str1 = "2018年5月17日 08:08:08";
String str2 = "2018-5-17 08:08:08";
String str3 = "2018/5/17 08:08:08";
method_2(str1);
}
// 格式化 : 日期 -> 字符串
public static void method_1(Date date) {
// 1. 创建一个日期格式化对象
DateFormat df = new SimpleDateFormat();
// 2. 格式化 :
String str = df.format(date);
// 3. 查看
System.out.println(str);
}
// 解析 : 字符串 -> 日期
public static void method_2(String str) {
// 1. 创建一个日期格式化对象 (指定解析的模式)
SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
// 2. 解析
try {
Date date = df.parse(str);
System.out.println(date);
} catch (ParseException e) {
System.out.println("解析模式错误!");
}
}
}
3. Calendar 日历类
特点 : 可以单独获取 `年,月,日,时,分,秒` 每一个独立的方法.
1. 获取日历对象
2. 设置日历对象的属性.
3. 增加.
Calendar
提供了一个类方法 getInstance
,以获得此类型的一个通用的对象。Calendar
的 getInstance
方法返回一个 Calendar
对象,其日历字段已由当前日期和时间初始化:
Calendar rightNow = Calendar.getInstance();
package cn.panda.demo2;
import java.util.Calendar;
public class TestCalendar {
public static void main(String[] args) {
// 获取当前日期和时间表示的 `日历` 对象
Calendar now = Calendar.getInstance();
System.out.println(now);
// 获取数据 :
// get(field); 属性 : Calendar.YEAR
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH); // 0~11
int day = now.get(Calendar.DAY_OF_MONTH);
System.out.println("今天是 : " + year + "年" + (month+1) + "月" + day + "日");
// 设置
// void set(int field, int value)
now.set(Calendar.YEAR, 2008);
printCalendar(now);
// void set(int year, int month, int date)
// 2111年11月11日
now.set(2111, 10, 11);
printCalendar(now);
// 增加
// void add(int field, int amount);
// 需求 : 回到 10 年之前
now.add(Calendar.YEAR, -10);
printCalendar(now);
// 一个月之后
now.add(Calendar.MONTH, 1);
printCalendar(now);
}
public static void printCalendar(Calendar now) {
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH); // 0~11
int day = now.get(Calendar.DAY_OF_MONTH);
System.out.println("今天是 : " + year + "年" + (month+1) + "月" + day + "日");
}
}
15. 基本类型包装类
1. 包装类型产生的原因 :
回答 :基本数据类型不是类, 因此不能提供 `属性(存储数据) / 行为(操作数据)`. 所以Java语言为每一个基本数据提供了一个包装类, 用来给基本数据类型提供额外的操作功能.
基本数据类型 : 没有属性, 没有方法.
问题 :
- 1. int 类型存储的最大值和最小值分别是多少 ??? 2的15次方 -2的15次方
- 2. 如果将一个字符串 “998” 转换为基本数据类型 998 ???
- 3. 请问 998 的二进制, 八进制, 十六进制的字符串形式是什么 ???
- 4. 基本数据类型转字符串 ??? 998 + “”; (任何数据类型和字符串相加, 都会返回一个新字符串)
package cn.panda.demo3;
public class TestWrappingClass {
public static void main(String[] args) {
/*
1. int 类型存储的最大值和最小值分别是多少 ???
2. 如果将一个字符串 “998” 转换为基本数据类型 998 ???
3. 请问 998 的二进制, 八进制, 十六进制的字符串形式是什么 ???
*/
/* 基本数据类型 包装类型
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean
*/
// int 是 4 个字节, 1个字节等于8个比特位
int size = Integer.SIZE;
System.out.println(size); // 32
int maxValue = Integer.MAX_VALUE;
System.out.println(maxValue);
int minValue = Integer.MIN_VALUE;
System.out.println(minValue);
int bytes = Integer.BYTES;
System.out.println(bytes);
String hexString = Integer.toHexString(998);
String str = Integer.toString(998);
String octalString = Integer.toOctalString(998);
String binaryString = Integer.toBinaryString(998);
System.out.println(hexString);
System.out.println(str);
System.out.println(octalString);
System.out.println(binaryString);
// 解析 : parsexxx
int num = Integer.parseInt("998");
System.out.println(num + 2);
}
}
1. 基本数据类型与包装类型之间的转换 :
package cn.panda.demo3;
public class TestWrappingClass2 {
public static void main(String[] args) {
// 1. 基本数据类型转包装类型
Integer integer_num1 = new Integer(998);
Integer integer_num2 = Integer.valueOf(2);
// 2. 包装类型转基本数据类型
int num1 = integer_num1.intValue();
int num2 = integer_num2.intValue();
// 3. 基本数据类型转字符串
String str_num1 = 998 + "";
String str_num2 = String.valueOf(2);
String str_num3 = Integer.toString(98);
// 4. 字符串转基本数据类型
int num3 = Integer.parseInt(str_num1);
// 5. 包装类型转字符串
String str_num4 = integer_num1.toString();
// 6. 字符串转包装类型
Integer integer_num3 = new Integer("123");
Integer integer_num4 = Integer.valueOf("456");
}
}
3. 自动装箱与自动拆箱 :
自动转箱 : 将基本数据自动包装为引用类型.
自动拆箱 : 将引用类型自动拆解为基本数据类型.
package cn.panda.demo3;
public class TestWrappingClass3 {
public static void main(String[] args) {
// 自动转箱 : 将基本数据自动包装为引用类型.
// Integer num1 = Integer.valueOf(998);
Integer num1 = 998;
// 自动拆箱 : 将引用类型自动拆解为基本数据类型.
// int num2 = Integer.intValue(num1);
int num2 = num1;
System.out.println(num1 + num2);
}
}
4. 面试题说明 :
说明 : 根据传入 i 的数值, 进行判断, 如果 i >= 整型缓存类.最小值 && i <= 整型缓存类.最大值, 如果满足, 返回整型缓存类中数组内对应的对象, 否则就创建一个新的包装类对象.
整型缓冲类的范围 : range [-128, 127]
package cn.panda.demo3;
public class TestWrappingClass4 {
public static void main(String[] args) {
// IntegerCache 整型缓冲区范围 : -128 ~ 127 (最频繁) 自动装箱的技术可以节省大量的内存.
// 面试题 :
// 没有使用 `自动装箱` 的技术.
Integer num1 = new Integer(127);
Integer num2 = new Integer(127);
System.out.println(num1 == num2); // 地址 == 地址 false
// 使用了 `自动装箱` 的技术. Integer.valueOf(127);
Integer num3 = 127;
Integer num4 = 127;
System.out.println(num3 == num4); // 地址 == 地址 true
// 使用了 `自动装箱` 的技术.
Integer num5 = 128;
Integer num6 = 128;
System.out.println(num5 == num6); // 地址 == 地址 false
}
}
16. foreach / for in 循环
foreach 循环底层是迭代器, 迭代器在迭代同时千万不要使用 `集合进行增删改` 操作.
// foreach 循环只能遍历集合.
for (String name : c) {
if (name.equals("刘德华")) {
// 迭代器在迭代的同时, 不允许集合对象对元素进行 `增删改` 操作.
c.remove(name); // 错误! `并发修改异常`
}
}
package cn.panda.demo3;
import java.util.ArrayList;
public class TestForeach {
public static void main(String[] args) {
int[] arr = {11, 22, 33, 44, 55, 66, 77, 88, 99};
// fori 循环, i 就是下标
// 1. 循环初始化变量 2. 循环条件 3. 循环增量
// 集合 : 使用 for i 对集合中的元素进行 `增删改` 操作.
for (int i = 0; i < arr.length; i++) {
// 需求 : 对奇数下标元素 += 10, 对偶数下标元素 -= 10
if (i % 2 == 1) {
arr[i] += 10;
} else {
arr[i] -= 10;
}
System.out.println(arr[i]);
}
System.out.println("------------");
// foreach 循环 : 集合中, 我们会经常使用 foreach 循环遍历集合元素.
/*
* for (元素类型 元素名称 : 数组 / 集合) { 元素名称 }
*/
// 集合 : 使用 foreach 对集合中的元素遍历时, 不可以对集合元素进行 `增删改` 操作.
for (int num : arr) {
System.out.println(num);
}
System.out.println("-------------");
// 集合 :
ArrayList<String> list = new ArrayList<String>();
list.add("Monday");
list.add("Tuesday");
list.add("Wednesday");
list.add("Thursday");
list.add("Friday");
list.add("Saturday");
list.add("Sunday");
for (String day : list) {
System.out.println(day);
if ("Sunday".equals(day)) {
// 添加
list.add("星期八");
}
}
}
}
17. 可变参数 : 底层就是数组
package cn.panda.demo3;
public class TestVariableArguments {
public static void main(String[] args) {
int sum = getSum(10, 20);
int sum2 = getSum(10, 20, 30);
int result = getSum(10, 20, 30, 40);
System.out.println(result);
int[] arr = {10, 20, 30, 40, 50, 60, 70, 80, 90};
int sum3 = getSum(arr);
System.out.println(sum3);
}
// 可变参数 : 底层就是数组 (传递参数非常灵活)
// 好处 : 调用的时候, 可以一个,一个参数单独传入, 也可以将一个数组传入.
// 注意点1 : 可变参数必须是参数列表的最后一位.
// The variable argument type (可变参数类型) int of the method getSum must be the last parameter (形参)
public static int getSum(int... array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}
/*
// 需求 : 定义一个方法, 实现两个整型数值的累计和
public static int getSum(int num1, int num2) {
return num1 + num2;
}
// 需求 : 定义一个方法, 实现三个整型数值的累计和
public static int getSum(int num1, int num2, int num3) {
return num1 + num2 + num3;
}
// 需求 : 定义一个方法, 实现多个整型数值的累计和
// Duplicate method 重写方法
public static int getSum(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}
*/
}
18. Collection 集合体系
集合就是一个容器. 是一个长度可变的容器, 底层就是可变数组.
ArrayList 类的底层使用 Object 类型的数组存储元素. 不可以存储基本数据类型.
Collection 集合的根接口 :
- List 子接口 (有序, 可重复) 可操作索引 index
- Set 子接口 (无序, 不可重复)
Collection 层次结构 中的根接口。Collection 表示一组对象,这些对象也称为 collection 的元素。因此意思也就是只能引用数据类型。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序的,而另一些则是无序的。JDK 不提供此接口的任何直接 实现:它提供更具体的子接口(如 Set 和 List)实现。
Map 接口 : (独立的接口)
将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射一个值。
此接口取代 Dictionary 类,后者完全是一个抽象类,而不是一个接口。
实现类 : 重点
- ArrayList 数组列表 特点 : 有序, 可重复. (查询快, 节省内容)
- HashSet 哈希无序表 特点 : 无序, 不可重复.
- HashMap 哈希映射表 特点 : 无序. 将一个键映射到一个值. (键值对) key = value (字典 Dictionary 前身)
请问 : HashSet 与 HashMap 之间是什么样的关系 ???
集合工具类 : Collections : sort 排序
- 自然排序 Comparable 接口. (排序规则基本固定)
- 比较器排序 Comparator 接口. (排序规则可能会根据不同需要产生不同变化)
哈希表 : 底层依赖与 `hashCode + equals` 方法.
了解特点 :
1. LinkedList
2. Vector
3. LinkedHashSet
4. Hashtable
5. LinkedHashMap
1. Collection接口中常用方法
package cn.panda.demo1;
import java.util.ArrayList;
import java.util.Collection;
public class TestCollection {
public static void main(String[] args) {
// 多态 :
// ArrayList<String> list = new ArrayList<String>();
Collection<String> c = new ArrayList<String>();
// 1. 增
c.add("刘德华");
c.add("张学友");
c.add("黎明");
c.add("郭富城");
c.add("渣渣辉");
c.add("郭德纲");
// 输出集合对象 (地址, 元素)
// 说明 : toString() 被重写了. ArrayList 类重写了 toString() 方法.
System.out.println(c);
// 2. 删
c.remove("郭德纲");
System.out.println(c);
// 3. 判断两个集合是否相等
Collection<String> c2 = new ArrayList<String>();
c2.add("刘德华");
c2.add("张学友");
c2.add("黎明");
c2.add("郭富城");
c2.add("渣渣辉");
boolean result = c.equals(c2);
// 结果 : true 说明 ArrayList 重写了 equals 方法.
System.out.println(result);
// hashCode 哈希值 : 如果两个集合元素内容相等, 哪么计算出来的哈希值也相同 (HashSet类中具体说明)
System.out.println(c.hashCode());
System.out.println(c2.hashCode());
// contains 包含
result = c.contains("郭富城");
System.out.println(result);
// isEmpty 判断集合是否为空
result = c.isEmpty();
System.out.println(result);
// clear 清空集合元素
c.clear();
System.out.println(c);
result = c.isEmpty();
System.out.println(result);
// size 集合元素个数
System.out.println(c2);
int size = c2.size();
System.out.println(size);
// toArray
Object[] objArr = c2.toArray();
for (int i = 0; i < objArr.length; i++) {
System.out.println(objArr[i]);
}
System.out.println("--------------");
for (Object obj : objArr) {
System.out.println(obj);
}
}
}
注意 : 集合中只能存储 `引用类型`, 不可以存储基本数据类型, 需要注意的是, 基本数据类型编译器可以自动完成包装的过程.
public class TestCollection2 {
public static void main(String[] args) {
// 集合类中需要定义一个泛型, 泛型都是引用类型, 不可以传递基本数据类型.
ArrayList<Integer> list = new ArrayList<Integer>();
// 自动装箱的技术.
list.add(10);
list.add(20);
list.add(30);
list.add(40);
list.add(50);
Object obj1 = new Integer(998);
Object obj2 = 998; // 自动转箱
}
}
2. 迭代器介绍和使用
2.1 Iterator 迭代器
请问 : Collection 中的 Iterator 方法返回一个 `接口类型`, 到底返回的是什么 ???
回答 : 返回的是该接口的实现类对象. 谁是该接口的实现类对象吗 ??? 取决与创建的集合对象.
public class TestCollection3 {
public static void main(String[] args) {
// 多态 :
Collection<String> c = new ArrayList<String>();
// 1. 添加元素
c.add("刘德华");
c.add("张学友");
c.add("黎明");
c.add("郭富城");
c.add("刘德华");
c.add("张学友");
c.add("渣渣辉");
c.add("郭德纲");
// 2. 通用方式遍历集合元素
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(name);
}
}
}
说明 : 调用 iterator 方法, 返回的是 ArrayList 内部类的一个对象, 这个对象实现了 Iterator 接口.
public class TestCollection3 {
public static void main(String[] args) {
// 多态 :
Collection<String> c = new HashSet<String>();
// 1. 添加元素
c.add("刘德华");
c.add("张学友");
c.add("黎明");
c.add("郭富城");
c.add("刘德华");
c.add("张学友");
c.add("渣渣辉");
c.add("郭德纲");
// 2. 通用方式遍历集合元素
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(name);
}
}
}
2.2 ListIterator 列表迭代器
列表迭代器具有 `增删改` 的方法. 而父接口 Iterator 只具备 `删除` 的方法.
共演示了三个功能 :
- 使用Iterator迭代器实现集合元素的正向迭代.
- 使用 ListIterator 迭代器实现了集合元素的迭代同是并对集合元素实现 `增删改` 的操作.
- 使用 ListIterator 迭代器实现了集合元素的逆向迭代, 注意获取列表迭代器时需设置光标的初始位置.
package cn.panda.demo3;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class TestListIterator {
public static void main(String[] args) {
/*
共演示了三个功能 :
1. 使用Iterator迭代器实现集合元素的正向迭代.
2. 使用 ListIterator 迭代器实现了集合元素的迭代同是并对集合元素实现 `增删改` 的操作.
3. 使用 ListIterator 迭代器实现了集合元素的逆向迭代, 注意获取列表迭代器时需设置光标的初始位置.
*/
List<String> list = new ArrayList<>();
list.add("刘德华");
list.add("张学友");
list.add("郭富城");
list.add("黎明");
list.add("迪丽热巴");
list.add("古力娜扎");
// 1. 使用Iterator迭代器实现集合元素的正向迭代.
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String next = it.next();
System.out.println(next);
}
System.out.println("----------------");
// 2. 使用 ListIterator 迭代器实现了集合元素的迭代同是并对集合元素实现 `增删改` 的操作.
ListIterator<String> listIt = list.listIterator();
while (listIt.hasNext()) {
String name = listIt.next();
// 判断 :
// 需求一 : 在郭富城之后添加 `方媛`
if (name.equals("郭富城")) {
// 使用 `迭代器对象` 添加新元素
listIt.add("方媛");
} else if (name.equals("古力娜扎")) {
// 需求二 : 删除 `古力娜扎`
listIt.remove();
} else if (name.equals("黎明")) {
// 需求三 : 将 `黎明` 修改 `郭德纲`
listIt.set("郭德纲");
}
}
System.out.println(list);
System.out.println("----------------");
// 3. 使用 ListIterator 迭代器实现了集合元素的逆向迭代, 注意获取列表迭代器时需设置光标的初始位置.
// 正向 : hasNext() & next
// 逆向 : hasPrevious & previous
ListIterator<String> listIt2 = list.listIterator(list.size());
while (listIt2.hasPrevious()) {
String previous = listIt2.previous();
System.out.println(previous);
}
}
}
3. 迭代器使用注意细节
注意一 :
使用迭代器迭代集合的时候,每一个hasNext方法都对应一个next,不要一个hasNext方法对应多个next。
package cn.panda.demo1;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TestCollection4 {
public static void main(String[] args) {
// 多态 :
Collection<String> c = new ArrayList<String>();
// 1. 添加元素
c.add("刘德华");
c.add("张学友");
c.add("黎明");
c.add("郭富城");
c.add("渣渣辉");
c.add("郭德纲");
c.add("苍老师");
// 2. 通用方式遍历集合元素
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String name = it.next();
System.out.println(it.next());
}
}
}
注意二 :
`并发` 修改异常.
并发 : 指两个或两个以上的对象操作同一份数据.
package cn.panda.demo1;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TestCollection4 {
public static void main(String[] args) {
// 多态 :
Collection<String> c = new ArrayList<String>();
// 1. 添加元素
c.add("刘德华");
c.add("张学友");
c.add("黎明");
c.add("郭富城");
c.add("渣渣辉");
c.add("郭德纲");
c.add("苍老师");
// 2. 通用方式遍历集合元素
Iterator<String> it = c.iterator();
while (it.hasNext()) {
String name = it.next();
// 需求 : 将 `郭德纲` 删除
if (name.equals("苍老师")) {
// c.remove(name); 错误!
it.remove();
}
}
// 查看元素
System.out.println(c);
}
}
4. 模拟扑克牌洗牌
牌 : 2 3 4 5 6 7 8 9 10 J Q K A ♣ ♦ ♠ ♥ 小☺ 大☺
package cn.panda.demo2;
public class TestPoker {
public static void main(String[] args) {
// 2 3 4 5 6 7 8 9 10 J Q K A ♣ ♦ ♠ ♥ 小☺ 大☺
// 模拟斗地主 : 54 张扑克牌 :
// 2♣ 2♦ 2♠ 2♥ 3♣ 3♦ 3♠ 3♥ 4♣ 4♦ 4♠ 4♥ ...... 小☺ 大☺
// 1. 准备一副扑克牌
// 2. 洗牌
// 3. 发牌
// 4. 看牌
}
}
代码实现 :
package cn.panda.demo2;
import java.util.ArrayList;
import java.util.Collections;
public class TestPoker {
public static void main(String[] args) {
// 2 3 4 5 6 7 8 9 10 J Q K A ♣ ♦ ♠ ♥ 小☺ 大☺
// 模拟斗地主 : 54 张扑克牌 :
// 2♣ 2♦ 2♠ 2♥ 3♣ 3♦ 3♠ 3♥ 4♣ 4♦ 4♠ 4♥ ...... 小☺ 大☺
// 需求 : 准备一副扑克牌 (使用集合存储扑克牌)
// 1.1 定义一个数组列表集合, 用于存储扑克牌
ArrayList<String> pokers = new ArrayList<String>();
// 1.2 准备一个 `数字牌` 集合
ArrayList<String> numbers = new ArrayList<String>();
for (int i = 2; i <= 10; i++) {
numbers.add(i + "");
}
numbers.add("J");
numbers.add("Q");
numbers.add("K");
numbers.add("A");
// System.out.println(numbers);
// 1.3 准备一个 `花色牌` 集合
ArrayList<String> colors = new ArrayList<String>();
colors.add("♣");
colors.add("♦");
colors.add("♠");
colors.add("♥");
// System.out.println(colors);
// 1.4 拼接每一张扑克牌
for (String number : numbers) {
for (String color : colors) {
// 拼接
String poker = number + color;
// System.out.println(poker);
// 将拼接完成的 poker 添加到 pokers 数组中
pokers.add(poker);
}
}
// 1.5 最后添加大小王
pokers.add("小☺");
pokers.add("大☺");
// 2. 洗牌
// 说明 : Collections 集合工具类 : shuffle 置换
Collections.shuffle(pokers);
// System.out.println(pokers);
// 3. 发牌 (三个玩家, 一个底牌)
ArrayList<String> player1 = new ArrayList<String>();
ArrayList<String> player2 = new ArrayList<String>();
ArrayList<String> player3 = new ArrayList<String>();
ArrayList<String> base = new ArrayList<String>();
// 遍历扑克牌集合 (foreach / fori 下标判断)
for (int i = 0; i < pokers.size(); i++) {
// 取出当前的扑克牌
String poker = pokers.get(i);
// 判断底牌和玩家
if (i >= pokers.size() - 3) {
// 底牌
base.add(poker);
} else {
// 玩家
if (i % 3 == 0) {
// 玩家1
player1.add(poker);
} else if (i % 3 == 1) {
// 玩家2
player2.add(poker);
} else {
// 玩家3
player3.add(poker);
}
}
}
// 4. 看牌
System.out.println(player1);
System.out.println(player2);
System.out.println(player3);
System.out.println(base);
}
}
5. List接口的特有方法
对索引的操作index 增删改查
Collection 只有增和删add()/remove()
package cn.panda.demo3;
import java.util.ArrayList;
import java.util.List;
public class TestList {
public static void main(String[] args) {
// List 接口接收 : 泛型的自动推导
// ArrayList<String> list2 = new ArrayList<String>();
List<String> list = new ArrayList<>();
// add 方法是 Collection 接口定义的.
list.add("刘德华");
list.add("张学友");
list.add("郭富城");
list.add("黎明");
System.out.println(list);
// 需求 : 在刘德华之后添加柳岩
list.add(1, "柳岩");
System.out.println(list);
list.add(4, "方媛");
System.out.println(list);
list.add("郭德纲");
System.out.println(list);
// remove
list.remove(list.size() - 1);
System.out.println(list);
// set
list.set(1, "苍老师");
System.out.println(list);
// get
String name = list.get(4);
System.out.println(name);
}
}
6. 使用 List 存储自定义对象,并去除重复元素
package cn.panda.demo3;
import java.util.ArrayList;
public class TestArrayList {
public static void main(String[] args) {
// 需求 : 使用 List 存储自定义对象,并去除重复元素
/*
* list 特点 : 有序, 可重复. 使用下标完成的.
*/
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("刘德华", 18));
list.add(new Student("张学友", 19));
list.add(new Student("黎明", 16));
list.add(new Student("郭富城", 22));
list.add(new Student("张学友", 19));
list.add(new Student("黎明", 16));
list.add(new Student("刘德华", 18));
// 1. 创建一个空集合对象
ArrayList<Student> list2 = new ArrayList<Student>();
// 遍历 list 集合
for (Student stu : list) {
// 2. 判断一下当前取出的元素, 在 list2 集合中是否包含
if (list2.contains(stu) == false) {
// 3. 不包含, 将当前元素添加到 list2 集合中
list2.add(stu);
}
}
// 4. 清空 list 集合
list.clear();
// 5. 将 list2 集合中的所有元素, 全部添加到 list 集合中
// 说明 : addAll(Collection接口类型的参数); 如果一个方法的参数类型为接口, 其真正需要的是该接口的实现类对象.
list.addAll(list2);
// 6. 查看list集合中的所有元素
for (Student stu : list) {
System.out.println(stu);
}
}
}
7. HashSet 存储字符串的实现原理分析
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。
哈希表->hashtable/hashmap, 数组+链表 数组查询块增删慢 链表查询慢增删快
增删时不用移动全部的元素了,就放在索引的位置
请问 : 哈希表是如何保证元素的唯一性 ???
package cn.panda.demo4;
import java.util.HashSet;
public class TestHashSet {
public static void main(String[] args) {
/*
* Set 接口的特点:
* 1. 存取无序 / 迭代无序
* 2. 不可以存储重复元素
*/
HashSet<String> set = new HashSet<String>();
set.add("王昭君");
set.add("貂蝉");
set.add("西施");
set.add("杨玉环");
set.add("王昭君");
set.add("貂蝉");
set.add("西施");
set.add("杨玉环");
for (String name : set) {
System.out.println(name);
}
}
}
请问 : HashSet 底层由哈希表支持, 那哈希表到底是如何保证元素的唯一性 ???
回答 : 哈希表底层依赖于 `存储对象` 的 hashCode() + equals() 方法共同来保证存储元素的唯一性的. 两者缺一不可. 必须都要实现.
演示了哈希值的数值 :
package cn.panda.demo2;
public class TestHashCode {
public static void main(String[] args) {
// 字符串是常量, 两个常量的地址也是相同的.
int hashCode1 = "王昭君".hashCode();
int hashCode2 = "王昭君".hashCode();
System.out.println(hashCode1);
System.out.println(hashCode2);
// 说明 : String 类一定是重写 hashCode() 方法, 根据字符串内容计算哈希值.
// 如果字符串内容相同, 计算出来的哈希值就相同.
int hashCode5 = new String("王昭君").hashCode();
int hashCode6 = new String("王昭君").hashCode();
System.out.println(hashCode5);
System.out.println(hashCode6);
// 发现 : 如果 Student 类没有重写 Object 类的 hashCode() 方法.
// 此时, 哈希表底层会调用 Object 类的 hashCode() 方法, 根据对象的地址来计算哈希值.
// 如果这样, 两个对象的数据相同, 但由于计算 hashCode 值不相同, 因此导致了两个对象根据没有机会调用 equals 方法实现判断.
// 解决 : 如果两个对象的内容相同, 就一定要保证计算出来的哈希值相同, 如何实现 ??? Student 类重写 hashCode 方法.
int hashCode3 = new Student("王昭君", 18).hashCode();
int hashCode4 = new Student("王昭君", 18).hashCode();
System.out.println(hashCode3);
System.out.println(hashCode4);
}
}
说明 : 由于 Student 类没有重写 hashCode() 方法, 因此程序调用 Object 类的 hashCode() 方法实现, 而 Object 类的实现是根据对象的地址来计算哈希值的.
因为, 两个不同堆区中的对象, 无论数据是否相同, 但是由于地址不相同, 因此计算出来的哈希值就不相同.
8. HashSet 存储自定义对象保证不重复的实现原理分析
package cn.panda.demo4;
import java.util.HashSet;
public class TestHashSet2 {
public static void main(String[] args) {
/*
* Set 接口的特点:
* 1. 存取无序 / 迭代无序
* 2. 不可以存储重复元素
*/
HashSet<Student> set = new HashSet<Student>();
set.add(new Student("王昭君", 18));
set.add(new Student("貂蝉", 19));
set.add(new Student("西施", 17));
set.add(new Student("杨玉环", 16));
set.add(new Student("王昭君", 18));
set.add(new Student("貂蝉", 19));
set.add(new Student("西施", 17));
set.add(new Student("杨玉环", 16));
for (Student stu : set) {
System.out.println(stu);
}
}
}
问题1 : 为什么 HashSet 集合存储自定义对象没有去除重复元素呢 ???
说明 : Student 类已经重写了 equals 方法, 所以该问题与 contains 方法遇到的问题不一样.
思路 : 存储 String 类型的对象就不会出现重复, 而存储 Student 自定义对象出现重复, 所以 String 和 Student 一定有不同关键之处.
1. hashCode 方法.
2. equals 方法.
API说明: 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。这一般是通过将该对象的内部地址转换成一个整数来实现的.
自定义 Student 重写 hashCode 方法 :
// 重写 hashCode 方法
@Override
public int hashCode() {
// 数据内容相同, 哈希值就要相同 (name, age) String 类已经重写了 hashCode(), 因此我们可以在程序中直接调用String类的该方法.
return name.hashCode() + age;
}
再次测试哈希值 :
package cn.panda.demo2;
public class TestHashCode {
public static void main(String[] args) {
// 字符串是常量, 两个常量的地址也是相同的.
int hashCode1 = "王昭君".hashCode();
int hashCode2 = "王昭君".hashCode();
System.out.println(hashCode1);
System.out.println(hashCode2);
// 说明 : String 类一定是重写 hashCode() 方法, 根据字符串内容计算哈希值.
// 如果字符串内容相同, 计算出来的哈希值就相同.
int hashCode5 = new String("王昭君").hashCode();
int hashCode6 = new String("王昭君").hashCode();
System.out.println(hashCode5);
System.out.println(hashCode6);
// 发现 : 如果 Student 类没有重写 Object 类的 hashCode() 方法.
// 此时, 哈希表底层会调用 Object 类的 hashCode() 方法, 根据对象的地址来计算哈希值.
// 如果这样, 两个对象的数据相同, 但由于计算 hashCode 值不相同, 因此导致了两个对象根据没有机会调用 equals 方法实现判断.
// 解决 : 如果两个对象的内容相同, 就一定要保证计算出来的哈希值相同, 如何实现 ??? Student 类重写 hashCode 方法.
int hashCode3 = new Student("王昭君", 18).hashCode();
int hashCode4 = new Student("王昭君", 18).hashCode();
System.out.println(hashCode3);
System.out.println(hashCode4);
}
}
自定义对象实现了根据数据内容来计算哈希值, 保证了数据相同, 计算出来哈希值就相等. 因此也可以保证 equals 方法的调用.
相同的数据内容, 哈希值一定相等(应该相等,所以重写equals就重写hashcode)
哈希值相同, 内容不一定相同,
Student 类重写 hashCode 方法 :
package cn.panda.demo2;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
// 重写 hashCode 方法
@Override
public int hashCode() {
// 数据内容相同, 哈希值就要相同 (name, age)
return name.hashCode() + age;
}
// 重写 equals 方法.
@Override
public boolean equals(Object obj) {
System.out.println("-----------------");
// 1. 比较两个对象的地址值
if (this == obj) {
return true;
}
// 2. 判断 obj 的具体类型
if (obj instanceof Student) {
// 2.2 强制转换
Student stu = (Student) obj;
// 2.3 判断 name 和 age
if (this.name.equals(stu.name) && this.age == stu.age) {
return true;
}
}
// 3. 返回 false
return false;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package cn.panda.demo2;
import java.util.HashSet;
public class TestHashSet2 {
public static void main(String[] args) {
/*
* Set 接口的特点:
* 1. 存取无序 / 迭代无序
* 2. 不可以存储重复元素
*/
HashSet<Student> set = new HashSet<Student>();
set.add(new Student("王昭君", 18));
set.add(new Student("貂蝉", 19));
set.add(new Student("西施", 17));
set.add(new Student("杨玉环", 16));
set.add(new Student("王昭君", 18));
set.add(new Student("貂蝉", 19));
set.add(new Student("西施", 17));
set.add(new Student("王昭君", 18));
set.add(new Student("杨玉环", 16));
for (Student stu : set) {
System.out.println(stu);
}
}
}
结果表明 : equals 方法被调用了四次.
HashSet 底层存储元素图解 :
9. LinkedList 链表集合 (了解)
List 接口的链接列表实现。实现所有可选的列表操作,并且允许所有元素(包括 null)。除了实现 List 接口外,LinkedList类还为在列表的开头及结尾 get、remove和 insert元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。
LinkedList 类头尾操作非常方便 :
package cn.panda.demo3;
import java.util.LinkedList;
public class TestLinkedList {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<String>();
list.add("王昭君");
list.add("貂蝉");
list.add("西施");
list.add("杨玉环");
// 向开头添加柳岩
list.addFirst("柳岩");
// 向尾部添加雷锋
list.addLast("雷锋");
// 获取柳岩, 雷锋
String first = list.getFirst();
String last = list.getLast();
System.out.println(first + " : " + last);
// 移除柳岩和雷锋
String removeFirst = list.removeFirst();
String removeLast = list.removeLast();
System.out.println(removeFirst + " : " + removeLast);
System.out.println(list);
}
}
Java 语言的四种结构:
1. 数组结构
2. 链表结构
a) 单链表
b) 双链表
3. 栈区结构
4. 队列结构
双链表 :
1. 栈 (先进后出)
2. 队列 (先进先出)
10. Java语言中的数据存储特点图解 :
19. 泛型 :
请问 : 什么叫泛型 ??? 参数化类型就是泛型. (以传参的方式确定泛型的真正类型)
GenericClass1 is a raw type. References to generic type GenericClass1<T> should be parameterized.
该类是一个原类型, 但引用指向了一个 `泛型类`. 泛型类的泛型应该被 `参数化`.
Element 元素
Key -> Value 键值对
Type 类型
1. 自定义泛型类介绍
package cn.panda.demo4;
// 定义了一个 `泛型` 类
// 在一个类上定义泛型的目的是什么 ??? 为了在类的内部使用.
// 类的 T 泛型, 什么时候确定 ??? 创建对象的时候进行确定.
public class GenericClass1<T> {
// 属性 :
private T field;
// 行为 :
public void setField(T field) {
this.field = field;
}
public T getField() {
return field;
}
}
package cn.panda.demo4;
// 定义了带两个泛型的类 `HashMap`
public class GenericClass2<K, V> {
private K key;
private V value;
// 行为 :
public void setKey(K key) {
this.key = key;
}
public K getKey() {
return key;
}
public void setValue(V value) {
this.value = value;
}
public V getValue() {
return value;
}
}
测试类 :
package cn.panda.demo4;
public class TestGenericType {
public static void main(String[] args) {
GenericClass1<String> g1 = new GenericClass1<>(); 自动推导
g1.setField("hello world");
String str = g1.getField();
System.out.println(str);
System.out.println("--------------------");
GenericClass1<Integer> g2 = new GenericClass1<Integer>();
g2.setField(998);
Integer number = g2.getField();
System.out.println(number);
System.out.println("--------------------");
GenericClass2<String, Integer> g3 = new GenericClass2<String, Integer>();
g3.setKey("Jack");
g3.setValue(30);
String name = g3.getKey();
Integer age = g3.getValue();
System.out.println(name + " = " + age);
}
}
2. 泛型接口的介绍和演示
请问 : 接口中定义泛型的目的是什么 ??? 让实现类进行类型的真正确定.
package cn.panda.demo4;
public interface MyInterface<T> {
void show(T t);
}
package cn.panda.demo4;
// class Student implements Comparable<Student> {}
public class MyInterfaceImpl1 implements MyInterface<String> {
@Override
public void show(String t) {
System.out.println(t);
}
}
package cn.panda.demo4;
// 如果一个类, 实现接口, 接口中有泛型, 实现类并没有指定, 那么该实现类必须是一个泛型类.
public class MyInterfaceImpl2<T> implements MyInterface<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
测试类 :
package cn.panda.demo4;
public class TestMyInterface {
public static void main(String[] args) {
// 多态 :
// 实现一 : 接口的实现没有定义泛型, 创建对象时, 无需指定
MyInterface<String> inter1 = new MyInterfaceImpl1();
inter1.show("Hello World.");
System.out.println("--------------------");
// 实现二 : 创建实现类的同时指定泛型的具体类型
MyInterfaceImpl2<String> inter2 = new MyInterfaceImpl2<String>();
inter2.show("你好, 世界!");
System.out.println("--------------------");
MyInterfaceImpl2<Integer> inter3 = new MyInterfaceImpl2<Integer>();
inter3.show(998);
}
}
3. 泛型方法的介绍
泛型方法 : 定义一个功能, 实现打印对象
package cn.panda.demo4;
public class Person {
// 属性
private String name;
private int age;
// 行为 :
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package cn.panda.demo4;
public class Animal {
// 属性
private String name;
private int age;
// 行为 :
@Override
public String toString() {
return "Animal [name=" + name + ", age=" + age + "]";
}
public Animal(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试类 :
package cn.panda.demo4;
public class TestGenericMethod {
public static void main(String[] args) {
Person p = new Person("张三", 18);
printInfo(p);
Animal a = new Animal("哮天犬", 100);
printInfo(a);
}
// 定义一个泛型方法, 如果在参数列表中需要使用, 那么该泛型需要使用 <> 定义.
// 该方法在编译器, 编译完成后, 会形成以下两个方法.
public static <T> void printInfo(T t) {
System.out.println(t);
}
// 定义一个方法, 输出对象 (Person / Animal) 的信息
/* 以下两个方法可以同时存在, 因此编译不报错!!!
public static void printInfo(Person p) {
System.out.println(p);
}
public static void printInfo(Animal a) {
System.out.println(a);
} */
}
4. 泛型通配符 `?` :
泛型通配符 : 定义一个功能, 实现遍历集合, 并打印对象
泛型 T, 由调用方法者传入类型时确定.
// 定义了一个泛型方法
public static <T> void printCollection(Collection<T> c) {
Iterator<T> it = c.iterator();
while (it.hasNext()) {
T t = it.next();
System.out.println(t);
}
}
? 通配符 : 可以完成的类型的限制.
? extends Object. 我不知道是什么类型,但是我知道该类型继承自 Object 类.
集合中基本只存储一种类型, 因此 `上限 / 下限` 仅作为了解 :
类型上限 :
? extends Person; 我不知道是什么类型,但是我知道该类型继承自 Person 类. (Student, Teacher, SchoolMaster 继承 Person, 因此这些类型都可以传入)
类型下限 :
? super ArmyDog; 我不知道是什么类型, 但是我知道该类型是 ArmyDog 的父类. (Animal, Dog, Cat, ArmyDog 这些类中, 只有 Cat 不可以传入,
因为 Cat 不是 ArmyDog 的父类.)
说明 : 程序开发中, 真正使用时, 一个集合中仅应该存储一种类型的元素. 不应该存储多种类型的元素.
// 统配符 : ? (不知道) 配置符只能用在泛型中, 不可以用在类型上.
// ? extends Object 通配符默认继承 Object 类.
public static void printCollection(Collection<?> c) {
Iterator<?> it = c.iterator();
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
}
测试代码实现 :
package cn.panda.demo4;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TestGenericMetho2 {
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<Person>();
list.add(new Person("张三", 19));
list.add(new Person("李四", 17));
list.add(new Person("王五", 29));
printCollection(list);
System.out.println("------------------");
ArrayList<Animal> list2 = new ArrayList<Animal>();
list2.add(new Animal("旺财", 30));
list2.add(new Animal("灰太狼", 20));
list2.add(new Animal("喜洋洋", 10));
printCollection(list2);
}
// 统配符 : ? (不知道) 配置符只能用在泛型中, 不可以用在类型上.
// ? extends Object 通配符默认继承 Object 类.
public static void printCollection(Collection<?> c) {
Iterator<?> it = c.iterator();
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
}
}
5. 泛型技术的小结 :
请问 : 泛型技术的出现带来了什么好处 ???
/*
* 1. 泛型技术可以确定集合中元素的统一类型.
* 2. 泛型可以避免类型的强转. (取出元素时, 无需进行类型强转)
* 3. 泛型将运行阶段的类型转换异常, 提前到了编译阶段进行检查. (编译不报错, 运行绝对不会报错)
* 4. 使用泛型技术, 就可以将类型推迟到 `创建对象` 时进行指定, 因此类型变得非常灵活.
*
* 注意点 : 泛型是编译时期的技术, 也就是说, 在程序运行阶段, 不存在任何泛型.
*/
package cn.panda.demo4;
import java.util.ArrayList;
import java.util.Iterator;
public class TestGenericType2 {
public static void main(String[] args) {
/*
String[] strs = new String[4];
strs[0] = "hello";
strs[1] = "world";
strs[2] = 998; // 编译报错, 类型不匹配.
*/
// 说明1 : 如果一个带泛型的类, 创建时, 不指定泛型, 此时, 泛型就会被提升为 Object 类型.
// 集合是数组的升级版. 底层就是一个可变的数组. 那么集合是通过什么来确定元素的唯一类型.
ArrayList<String> list = new ArrayList<String>();
list.add("hello");
list.add("world");
// list.add(998);
// list.add(true);
Iterator<String> it = list.iterator();
while (it.hasNext()) {
// Object obj = it.next();
// 强转
// String str = (String) obj;
String str = it.next();
System.out.println("str = " + str + ", len = " + str.length());
}
}
}
20. Map 映射表集合
Map 映射表 / 键值对 (key -> value)
将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射一个值。
“Jack” -> 18
“Jack” -> 30
实现了如果 key 相同, 让新值覆盖旧值.
如果键一样, 新值就会覆盖旧值.
1. Map接口中的方法演练
增删改查 根据key
Size/isempty/clear
containsKey/containValue
Keyset/ values/ entrySet
Map 和 Collection 接口之间没有任何关系.
package cn.panda.demo1;
import java.util.HashMap;
import java.util.Map;
public class TestMap {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
// put
map.put("Jack", 18);
map.put("Rose", 16);
map.put("LiLy", 14);
map.put("Lucy", 17);
map.put("Tom", 19);
map.put("Jerry", 20);
map.put("Jack", 30);
// get
Integer age = map.get("Jack");
System.out.println(age);
// remove
map.remove("Jack");
// size
int size = map.size();
System.out.println(size);
System.out.println(map);
// containsKey & containsValue
boolean r = map.containsKey("Rose");
System.out.println("r = " + r);
boolean r2 = map.containsValue(18);
System.out.println("r2 = " + r2);
// isEmpty
boolean result = map.isEmpty();
System.out.println(result);
// clear
map.clear();
System.out.println(map);
result = map.isEmpty();
System.out.println(result);
}
}
2. Map接口中的 keyset & values
package cn.panda.demo1;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class TestMap2 {
public static void main(String[] args) {
/*
* 请问 : 为什么 keySet 返回 Set 集合, 而 values 返回 Collection 集合 ???
* 1. Set 集合不可重复.
* 2. Collection 集合中的 list 是可以重复的.
*/
Map<String, Integer> map = new HashMap<>();
map.put("Jack", 18);
map.put("Rose", 14);
map.put("LiLy", 14);
map.put("Lucy", 17);
map.put("Tom", 19);
map.put("Jerry", 20);
map.put("Jack", 30);
// keySet 键集
Set<String> keySet = map.keySet();
Iterator<String> it = keySet.iterator();
while (it.hasNext()) {
String key = it.next();
// 请问 : 如何通过 key 获取 value ??? Map接口的get(key)方法实现
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
System.out.println("-------------------");
// values 值集
Collection<Integer> values = map.values();
Iterator<Integer> it2 = values.iterator();
while (it2.hasNext()) {
Integer value = it2.next();
// 请问 : 有没有办法通过 value 来获取 key ??? 没有办法.
System.out.println(value);
}
}
}
3. map接口中的 – entrySet (重点)
请问1 : HashSet 与 HashMap 之间是什么样的关系 ???
请问2 : EntrySet 到底是什么 ???
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。
package cn.panda.demo1;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
public class TestMap3 {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
// put 方法查看 HashMap 底层到底是如何存储数据的 ???
// 说明 : 我们将 key 和 value 传入到 HashMap 中, 底层就可以实现 key 和 value 的映射关系. 怎么做的 ???
map.put("Rose", 14);
map.put("LiLy", 14);
map.put("Lucy", 17);
map.put("Tom", 19);
map.put("Jerry", 20);
map.put("Jack", 30);
// entrySet 方法.
// 请问 : 为什么 entrySet() 返回 Set<Entry<String, Integer>> 该类型的集合 ??? Entry 到底是什么 ???
// JDK 1.7 : Entry 类
// JDK 1.8 : Node 类
// 1. entrySet `键值对` 集合, 因此该方法应该返回一个 Set 集合.
// 2. Set 集合有泛型, Set 集合中存储的是什么类型的对象 ??? Entry 类型的对象. 因此 Set 集合的泛型为 Entry 类型.
// 3. Entry类有两个泛型, 这两个泛型需要跟 `Map集合` 保持一致. 因此 Entry 是 Map 集合的内部类. 泛型一致
Set<Entry<String, Integer>> entrySet = map.entrySet();
Iterator<Entry<String, Integer>> it = entrySet.iterator();
while (it.hasNext()) {
Entry<String, Integer> entry = it.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
System.out.println("---------------");
for (Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
4. HashMap的简单演示 : 国家 -> 首都
package cn.panda.demo1;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
public class TestMap4 {
public static void main(String[] args) {
// 中国 -> 北京
HashMap<String, String> map = new HashMap<String, String>();
map.put("中国", "北京");
map.put("美国", "华盛顿");
map.put("俄罗斯", "莫斯科");
map.put("法国", "巴黎");
map.put("英国", "伦敦");
// 遍历一 : entrySet + foreach
for (Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + " = " + value);
}
System.out.println("-----------------");
// 遍历二 : keySet + iterator
Set<String> keySet = map.keySet();
Iterator<String> it = keySet.iterator();
while (it.hasNext()) {
String key = it.next();
String value = map.get(key);
System.out.println(key + " = " + value);
}
}
}
5. 自定义对象作为HashMap的key值
重写equals() 和 hascode()
确保key的唯一性
new Student("Jack", 30), "上海"
package cn.panda.demo1;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
// hashSet + equals (数据)
// alt + shift + s 自动生成 `hashCode + equals` 方法
@Override
public int hashCode() {
return name.hashCode() + age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Student) {
Student stu = (Student) obj;
if (this.name.equals(stu.name) && this.age == stu.age) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试类 :
package cn.panda.demo1;
import java.util.HashMap;
import java.util.Map.Entry;
public class TestMap5 {
public static void main(String[] args) {
// 需求 : 自定义对象作为 HashMap 的 key.
// new Student("Jack", 30), "上海"
// Map 的特点 : key 不能重复, 如果重复, 新值就会覆盖旧值
// HashSet 其实就是用了 HashMap 的 key.
// HashSet 是如何保证元素唯一性的 ??? 通过 `HashCode + equals` 共同保证元素的唯一性的.
// HashMap 唯一性其实就是针对 key 而言的.
HashMap<Student, String> map = new HashMap<Student, String>();
map.put(new Student("Jack", 30), "上海");
map.put(new Student("Rose", 20), "北京");
map.put(new Student("Lucy", 18), "纽约");
map.put(new Student("Lily", 22), "华盛顿");
map.put(new Student("Rose", 20), "北京");
map.put(new Student("Lucy", 18), "纽约");
map.put(new Student("Lily", 22), "华盛顿");
map.put(new Student("Jack", 30), "香港");
// 遍历
for (Entry<Student, String> entry : map.entrySet()) {
Student key = entry.getKey();
String value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
6. 练习: HashMap 集合嵌套 ArrayList 集合存储数据
"练习 : HashMap 集合嵌套 ArrayList 集合,并实现遍历 :"
元素内容 :
"四大天王 :"
歌神 - 张学友
综合 - 刘德华
舞王 - 郭富城
文艺 - 黎明
"四大美女 :"
沉鱼 - 西施浣纱
落雁 - 昭君出塞
闭月 - 貂蝉拜月
羞花 - 贵妃醉酒
"四大名捕"
冷血 - 邓超
无情 - 刘亦菲
铁手 - 邹兆龙
追命 - 郑中基
package cn.panda.demo2;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
public class TestPractice1 {
public static void main(String[] args) {
// 1. 创建一个映射表集合对象
HashMap<String, ArrayList<Star>> map = new HashMap<String, ArrayList<Star>>();
// 2. 创建一个 ArrayList 数组列表集合
ArrayList<Star> list = new ArrayList<Star>();
list.add(new Star("歌神", "张学友"));
list.add(new Star("综合", "刘德华"));
list.add(new Star("舞王", "郭富城"));
list.add(new Star("文艺", "黎明"));
// 3. 将 list 集合与字符串进行映射
map.put("四大天王", list);
// 4. 遍历一 : entrySet + foreach
for (Entry<String, ArrayList<Star>> entry : map.entrySet()) {
String key = entry.getKey();
System.out.println(key);
ArrayList<Star> value = entry.getValue();
// 遍历 list 集合
for (Star star : value) {
System.out.println("\t" + star);
}
}
System.out.println("-------------------");
// 5. 遍历二 : keySet + Iterator
Set<String> keySet = map.keySet();
Iterator<String> it = keySet.iterator();
while (it.hasNext()) {
String key = it.next();
System.out.println(key);
ArrayList<Star> value = map.get(key);
Iterator<Star> it2 = value.iterator();
while (it2.hasNext()) {
Star star = it2.next();
System.out.println("\t" + star);
}
}
}
}
7. Collections 集合工具类练习
此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。
数组的元素逆向排序
可以先把数组变成集合 Arrays.aslist()
package cn.panda.demo2;
import java.util.ArrayList;
import java.util.Collections;
public class TestPractice2 {
public static void main(String[] args) {
// Collections 集合工具类
// 1. addAll 添加所有
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println(list);
// 2. max / min
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(88);
list2.add(66);
list2.add(99);
list2.add(33);
list2.add(55);
list2.add(11);
Integer max = Collections.max(list2);
Integer min = Collections.min(list2);
System.out.println(max);
System.out.println(min);
// 3. sort 排序
System.out.println("排序前 : " + list2);
Collections.sort(list2);
System.out.println("排序后 : " + list2);
// 4. reverse 反转
Collections.reverse(list2);
System.out.println("反转后 : " + list2);
// 5. shuffle 置换
Collections.shuffle(list2);
System.out.println("置换后 : " + list2);
}
}
8. 自然排序规则 (重点)
自然排序底层依赖与 Comparable 接口.
问题 : 该 sort 方法对集合中的元素有要求, 集合元素必须要实现 Comparable 接口.
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。
如果自定义类想要具体 `可比较` 的能力, 那么该自定义类就必须实现 Comparable 接口. 因为该接口就是用来定义 `自然比较的规则`.
字符串的排序结果查看 :
package cn.panda.demo3;
public class TestString {
public static void main(String[] args) {
// 字符串比较是一个字符一个字符进行比较的,一旦有结果, 立刻返回, 之后的所有字符都不再比较.
String str1 = "cbcd"; // 98
String str2 = "abcd"; // 97
/*
* 请问 : 为什么 compareTo 返回的是一个 int 类型的结果 ???
* 1. 正数 调用对象比传入对象大
* 2. 负数 调用对象比传入对象小
* 3. 等于 0 表示两个比较的对象完全相同.
*/
int result = str1.compareTo(str2);
System.out.println(result);
}
}
解决方案 : Student 类在 compareTo 方法中实现 `比较的规则`.
public class Student implements Comparable<Student> {
@Override
public int compareTo(Student o) {
// 1. 先比年龄
int result = this.age - o.age;
// 2. 如果年龄一样, 再比姓名 (String 类已经实现了 Comparable 接口)
if (result == 0) {
// 3. 如果年龄相等, 根据名字进行排序
result = this.name.compareTo(o.name);
}
return result;
}
具体代码实现 :
package cn.panda.demo3;
public class Student implements Comparable<Student> {
@Override
public int compareTo(Student o) {
// 1. 先比年龄
int result = this.age - o.age;
// 2. 如果年龄一样, 再比姓名 (String 类已经实现了 Comparable 接口)
if (result == 0) {
// 3. 如果年龄相等, 根据名字进行排序
result = this.name.compareTo(o.name);
}
return result;
}
// 属性
private String name;
private int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
9. 比较器排序规则 (重点)
比较器排序底层依赖于 Comparator 接口.
比较器对象在 sort 方法的第二个参数传入.
package cn.panda.demo3;
public class Student2 {
// 属性
private String name;
private int age;
int id;
public Student2(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student2() {
super();
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package cn.panda.demo3;
import java.util.Comparator;
public class MyComparator implements Comparator<Student2> {
@Override
public int compare(Student2 o1, Student2 o2) {
// 1. 年龄
int result = o1.getAge() - o2.getAge();
// 2. 姓名
if (result == 0) {
result = o1.getName().compareTo(o2.getName());
}
return result;
}
}
package cn.panda.demo3;
import java.util.ArrayList;
import java.util.Collections;
public class TestStudentSort2 {
public static void main(String[] args) {
// 需求 : 先按年龄排, 如果年龄一样, 再按姓名排 .
ArrayList<Student2> list = new ArrayList<Student2>();
list.add(new Student2("刘德华", 19));
list.add(new Student2("黎明", 16));
list.add(new Student2("郭富城", 52));
list.add(new Student2("张学友", 18));
list.add(new Student2("柳岩", 19));
list.add(new Student2("刘亦菲", 18));
list.add(new Student2("杨幂", 33));
list.add(new Student2("方缘", 31));
// 年龄从小到大
// 说明 : 如果一个参数是接口类型的, 其真正需要的是该接口的实现类对象
// MyComparator 类需要满足 `实现 Comparator 接口`.
Collections.sort(list, new MyComparator());
for (Student2 stu : list) {
System.out.println(stu);
}
}
}
10. 匿名实现类完成比较器排序
格式 : new 接口() { 接口中抽象方法的实现体 };
// 年龄从小到大
// 说明 : 如果一个参数是接口类型的, 其真正需要的是该接口的实现类对象
// new Comparator() 接口不能实例化对象, 因为缺少实现体.
Collections.sort(list, new Comparator<Student2>(){
@Override
public int compare(Student2 o1, Student2 o2) {
System.out.println("-----------");
int result = o1.getAge() - o2.getAge();
if (result == 0) {
result = o1.getName().compareTo(o2.getName());
}
return result;
}
});
代码实现 :
package cn.panda.demo3;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class TestStudentSort3 {
public static void main(String[] args) {
// 需求 : 先按年龄排, 如果年龄一样, 再按姓名排 .
ArrayList<Student2> list = new ArrayList<Student2>();
list.add(new Student2("刘德华", 19));
list.add(new Student2("黎明", 16));
list.add(new Student2("郭富城", 52));
list.add(new Student2("张学友", 18));
list.add(new Student2("柳岩", 19));
list.add(new Student2("刘亦菲", 18));
list.add(new Student2("杨幂", 33));
list.add(new Student2("方缘", 31));
// 年龄从小到大
// 说明 : 如果一个参数是接口类型的, 其真正需要的是该接口的实现类对象
// new Comparator() 接口不能实例化对象, 因为缺少实现体.
Collections.sort(list, new Comparator<Student2>(){
@Override
public int compare(Student2 o1, Student2 o2) {
int result = o1.getAge() - o2.getAge();
if (result == 0) {
result = o1.getName().compareTo(o2.getName());
}
return result;
}
});
for (Student2 stu : list) {
System.out.println(stu);
}
}
}
11. Comparable 与 Comparator 的优先级补充 :
请问 : 如果存储的元素已经实现 `自然比较的规则`. 我们还可以改变该类元素的规则吗 ??? 可以.
如果一个元素的类即实现了 Comparable 接口, 同时也实现了 Comparator 接口, 此时, 程序底层就会以 Comparator 作为优先.
package cn.panda.demo1;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class ComparableAndComparator {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("dbc");
list.add("ebc");
list.add("abc");
list.add("cbc");
list.add("bbc");
Collections.sort(list); // String 类已经实现了 Comparable 接口
System.out.println(list);
System.out.println("-----------------");
Collections.sort(list, new Comparator<String>(){
@Override
public int compare(String o1, String o2) {
// o2 和 o1 进行换位
return o2.compareTo(o1);
}
});
System.out.println(list);
System.out.println("------------------");
ArrayList<String> list2 = new ArrayList<String>();
list2.add("Angle Baby");
list2.add("Andy");
list2.add("Jack ma");
list2.add("Mickel Jackson");
list2.add("Ann");
Collections.sort(list2);
System.out.println(list2);
System.out.println("------------------");
Collections.sort(list2, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
System.out.println(list2);
}
}
12. 斗地主版本二 : 已排序 Map集合实现
package cn.panda.demo4;
public class TestPoker2 {
public static void main(String[] args) {
// 1. 准备一副扑克牌 `映射表` key (数字) -> value (牌)
// 3♣ 3♦ 3♠ 3♥ 4♣ 4♦ 4♠ 4♥ ...... 2♣ 2♦ 2♠ 2♥ 小☺ 大☺
// 0 1 2 3 4 5 6 7 48 49 50 51 52 53
// 2. 准备一副发给玩家的 `数字牌`
// 3. 洗牌 Collections.shuffle
// 4. 发牌 for i 循环
// 5. 给玩家手中的数字牌进行排序 Collections.sort();
// 6. 玩家根据手中已排序的数字从 `映射表` 中取牌 get(key) -> value
}
}
代码实现 :
package cn.panda.demo4;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
public class TestPoker2 {
public static void main(String[] args) {
// 1. 准备一副扑克牌 `映射表` key (数字) -> value (牌)
// 3♣ 3♦ 3♠ 3♥ 4♣ 4♦ 4♠ 4♥ ...... 2♣ 2♦ 2♠ 2♥ 小☺ 大☺
// 0 1 2 3 4 5 6 7 48 49 50 51 52 53
// 1.1 准备一个存储扑克牌的映射表
HashMap<Integer, String> map = new HashMap<Integer, String>();
// 1.2 准备一副 `数字牌`
ArrayList<String> numbers = new ArrayList<String>();
for (int i = 3; i <= 10; i++) {
numbers.add(i + "");
}
Collections.addAll(numbers, "J", "Q", "K", "A", "2");
// System.out.println(numbers);
// 1.3 准备一副 `花色牌`
ArrayList<String> colors = new ArrayList<String>();
Collections.addAll(colors, "♣", "♦", "♥", "♠");
// System.out.println(colors);
// 1.4 循环遍历
// 定义一个 map_key
int map_key = 0;
for (String number : numbers) {
for (String color : colors) {
String poker = number + color;
// 1.5 将拼接完成的 poker 存储到映射表中
map.put(map_key++, poker);
// System.out.println(poker);
}
}
map.put(map_key++, "小☺");
map.put(map_key, "大☺");
// System.out.println(map);
// 2. 准备一副发给玩家的 `数字牌`
ArrayList<Integer> players_num = new ArrayList<Integer>();
for (int i = 0; i < 54; i++) {
players_num.add(i);
}
// 3. 洗牌 Collections.shuffle
Collections.shuffle(players_num);
// System.out.println(players_num);
// 4. 发牌 for i 循环
ArrayList<Integer> player1 = new ArrayList<Integer>();
ArrayList<Integer> player2 = new ArrayList<Integer>();
ArrayList<Integer> player3 = new ArrayList<Integer>();
ArrayList<Integer> base = new ArrayList<Integer>();
for (int i = 0; i < players_num.size(); i++) {
// 取牌
Integer poker = players_num.get(i);
// 判断
if (i >= players_num.size() - 3) {
// 底牌
base.add(poker);
} else {
// 玩家
if (i % 3 == 0) {
// 玩家 1
player1.add(poker);
} else if (i % 3 == 1) {
// 玩家 2
player2.add(poker);
} else {
// 玩家 3
player3.add(poker);
}
}
}
// 5. 给玩家手中的数字牌进行排序 Collections.sort();
Collections.sort(player1);
Collections.sort(player2);
Collections.sort(player3);
Collections.sort(base);
// 6. 玩家根据手中已排序的数字从 `映射表` 中取牌 get(key) -> value
// System.out.println("--------------------------------------");
// System.out.println(player1);
for (Integer key : player1) {
String value = map.get(key);
System.out.print(value + " ");
}
System.out.println();
// System.out.println(player2);
for (Integer key : player2) {
String value = map.get(key);
System.out.print(value + " ");
}
System.out.println();
// System.out.println(player3);
for (Integer key : player3) {
String value = map.get(key);
System.out.print(value + " ");
}
System.out.println();
// System.out.println(base);
for (Integer key : base) {
String value = map.get(key);
System.out.print(value + " ");
}
System.out.println();
}
}
13. 其它集合类 (了解)
1. Vector
注:此接口的功能与 Iterator 接口的功能是重复的。此外,Iterator 接口添加了一个可选的移除操作,并使用较短的方法名。新的实现应该优先考虑使用 Iterator 接口而不是 Enumeration 接口。
从 Java 2 平台 v1.2 开始,此类改进为可以实现 List
接口,与新 collection 实现不同,Vector
是同步的。
同步 : 效率低, 数据安全. (多线程)
非同步 : 效率高. 如果多线程操作共享数据, 就有可能会发生数据错误, 不安全.
package cn.panda.demo1;
import java.util.Enumeration;
import java.util.Vector;
public class TestVector {
public static void main(String[] args) {
Vector<String> v = new Vector<String>();
v.addElement("刘德华");
v.addElement("陈冠希");
v.addElement("张柏芝");
v.addElement("郭德纲");
v.addElement("谢霆锋");
v.addElement("陈冠希");
Enumeration<String> elements = v.elements();
while (elements.hasMoreElements()) {
String element = elements.nextElement();
System.out.println(element);
}
}
}
结果 : Vector 是有序, 可重复的. 但是该类在 V1.2 之后被 ArrayList 类替代掉了.
2. LinkedHashSet
具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。
package cn.panda.demo1;
import java.util.LinkedHashSet;
public class TestLinkedHashSet {
public static void main(String[] args) {
// Linked 迭代有序 Hash 元素唯一 Set
LinkedHashSet<String> set = new LinkedHashSet<String>();
set.add("张柏芝");
set.add("张柏芝");
set.add("刘德华");
set.add("郭德纲");
set.add("谢霆锋");
set.add("谢霆锋");
set.add("陈冠希");
set.add("郭德纲");
set.add("陈冠希");
for (String name : set){
System.out.println(name);
}
}
}
结果 : LinkedHashSet 可以完成迭代有序 (链表) 并且元素唯一 (哈希表).
3. Hashtable
请问 : Hashtable 和 HashMap 有什么区别 ???
Hashtable 是同步的. (效率低, 安全) 不可以存储 null 元素.
HashMap 是非同步的. (效率高, 不安全) 可以存储 null 元素.
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null
对象都可以用作键或值。 为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode
方法和 equals
方法。
从Java 2 平台 v1.2起,此类就被改进以实现 Map
接口,不像新的 collection 实现,Hashtable
是同步的
package cn.panda.demo1;
import java.util.Hashtable;
import java.util.Map.Entry;
public class TestHashtable {
public static void main(String[] args) {
Hashtable<String, Integer> table = new Hashtable<String, Integer>();
table.put("Jack", 18);
table.put("Peter", 19);
table.put("Lucy", 15);
table.put("Mery", 16);
table.put("Jack", 30);
for (Entry<String, Integer> entry : table.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
4. LinkedHashMap
Map 接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。
package cn.panda.demo1;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
public class TestLinkedHashMap {
public static void main(String[] args) {
LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>();
map.put("Peter", 19);
map.put("Lucy", 15);
map.put("Jack", 18);
map.put("Mery", 16);
map.put("Jack", 30);
for (Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}
18. File 文件 / 文件夹
文件和目录 (文件夹) 路径名的抽象表示形式。
1. File类的构造方法 :
// File 类的构造方法
// 1. File(File parent, Stirng child)
// 2. File(String pathname)
// 3. File(String parent, String child)
package cn.panda.demo2;
import java.io.File;
public class TestFile {
public static void main(String[] args) {
// 请问 : 如果让一个 File 对象与硬盘的 `文件/ 文件夹` 进行关联.
// File 类的构造方法
// 1. File(String pathname)
File file1 = new File("C:\\Users\\Jack\\Desktop\\Day10");
System.out.println(file1);
// 2. File(File parent, Stirng child)
File file2 = new File(file1, "hello.txt");
System.out.println(file2);
// 3. File(String parent, String child)
File file3 = new File("C:\\Users\\Jack\\Desktop\\Day10", "hello.txt");
System.out.println(file3);
}
}
2. 创建文件 / 文件夹
package cn.panda.demo2;
import java.io.File;
import java.io.IOException;
public class TestCreateNewFile {
public static void main(String[] args) {
// 1. 创建一个file对象, 与硬盘的路径进行关联
File file = new File("C:\\Users\\Jack\\Desktop\\Day10\\folder");
// 2. 创建文件
try {
// 说明 : 如果文件已经存在, 就不再创建, 返回 false.
boolean result = file.createNewFile();
System.out.println(result);
} catch (IOException e) {
System.out.println("文件创建失败!");
}
}
}
请问 : 后缀名到底是用来做什么的 ??? 后缀名其实就是用来给 `软件` 看的.
txt, 记事本默认可以识别该后缀名的文件. 如果打开, 默认就会以记事本软件打开.
png, jpg 图片软件默认可以识别该后缀名的文件. 如果打开, 默认就会以图片软件打开.
avi, mp4, flv 视频软件默认可以识别该后缀名的文件. 如果打开, 默认就会以视频软件打开.
mkdir -> make directory 创建目录 / 创建 `单级` 文件夹
mkdirs -> make directories 创建 `多级` 文件夹.
package cn.panda.demo2;
import java.io.File;
public class TestMakeDirectory {
public static void main(String[] args) {
File file = new File("C:\\Users\\Jack\\Desktop\\Day10\\folder");
// boolean result = file.mkdir();
boolean result = file.mkdirs();
System.out.println(result);
}
}
3. 删除方法
package cn.panda.demo2;
import java.io.File;
public class TestDelete {
public static void main(String[] args) {
File file = new File("C:\\Users\\Jack\\Desktop\\Day10\\folder2");
// 删除文件 (文件可以直接删除)
// 删除文件夹 (只能删除空文件夹, 如果文件夹非空, 无法删除)
boolean result = file.delete();
System.out.println(result);
}
}
4. 重命名方法
package cn.panda.demo2;
import java.io.File;
public class TestRenameTo {
public static void main(String[] args) {
File file = new File("C:\\Users\\Jack\\Desktop\\Day10\\课程资料\\ly.jpg");
File dest = new File("C:\\Users\\Jack\\Desktop\\柳岩.jpg");
// 说明 : 源文件和目的地文件父目录一致. 相当于 `重命名`
// 说明 : 源文件和目的地文件父目录不一致. 类似于 `先剪切, 再重命名`
boolean result = file.renameTo(dest);
System.out.println(result);
}
}
5. 判断方法
// isDirecoty
// isFile
// exists 判断文件是否存在
package cn.panda.demo2;
import java.io.File;
public class TestBoolean {
public static void main(String[] args) {
File file = new File("C:\\Users\\Jack\\Desktop\\Day10\\课程资料");
// isDirecoty
boolean result = file.isDirectory();
System.out.println(result);
// isFile
boolean result2 = file.isFile();
System.out.println(result2);
// exists 判断文件是否存在
boolean exists = file.exists();
System.out.println(exists);
}
}
6. 获取方法
// getAbsoluteFile
// getName
// getParentFile
// length
package cn.panda.demo2;
import java.io.File;
public class TestGet {
public static void main(String[] args) {
// 相对路径 : 没有根盘符的路径就是相对路径 (相对于当前的项目)
File file = new File("demo1.txt");
System.out.println(file);
// getAbsoluteFile
File absoluteFile = file.getAbsoluteFile();
System.out.println(absoluteFile);
// getName
String name = absoluteFile.getName();
System.out.println(name);
// getParentFile
File parentFile = absoluteFile.getParentFile();
System.out.println(parentFile);
// length
// 说明 : 一个汉字对应 `多个` 字节.
long length = file.length(); // 数据的 `字节数`
System.out.println(length);
// 判断文件是否存在
boolean result = file.exists();
System.out.println(result);
}
}
7. 练习 (判断.jpg后缀名的文件)
package cn.panda.demo2;
import java.io.File;
public class TestPractice {
public static void main(String[] args) {
// 练习 (判断.jpg后缀名的文件)
// 1. 创建一个 File 对象, 与硬盘的文件夹进行关联
File file = new File("C:/Users/Jack/Desktop/Day10/课程资料");
// 2. 使用 file 对象调用 list 方法, 列举文件夹中的所有 `文件和文件夹` 字符串
String[] names = file.list();
// 3. 遍历
for (String name : names) {
if (name.endsWith(".jpg")) {
// 创建一个文件对象
File f = new File(file, name);
// 打印所有 jpg 文件的绝对路径
System.out.println(f.getAbsoluteFile());
}
}
System.out.println("----------------------");
// listFiles(); 底层先调用 list() 方法, 获取所有的文件名称, 然后 new 文件对象, 将创建的文件对象转入到 File[]中, 并返回.
File[] files = file.listFiles();
for (File f : files) {
if (f.getName().endsWith(".java")) {
// .java
System.out.println(f);
}
}
}
}
8. 文件过滤器 (FilenameFilter & FileFilter)
自定义过滤文件名称的条件, 由外部创建时传入.
1. FilenameFilter 文件名称过滤器接口
实现此接口的类实例可用于过滤器文件名。
package cn.panda.demo3;
import java.io.File;
import java.io.FilenameFilter;
// 通用文件名称过滤器
public class MyFilenameFilter implements FilenameFilter {
// 属性
private String suffix;
// 构造方法
public MyFilenameFilter(String suffix) {
this.suffix = suffix;
}
//
@Override
public boolean accept(File dir, String name) {
// System.out.println("dir = " + dir); 父目录
// System.out.println("name = " + name); 文件名称
// 判断, 后缀名由外部传入
if (name.endsWith(suffix)) {
return true;
}
return false;
}
}
package cn.panda.demo3;
import java.io.File;
public class TestFilenameFilter {
public static void main(String[] args) {
// 需求 : 自定义过滤文件名称的条件, 由外部创建时传入.
// 1. 创建一个 File 对象, 与硬盘的文件夹进行关联
File file = new File("C:/Users/Jack/Desktop/Day10/课程资料");
// 2. 调用 list(FilenameFilter)
// MyFilenameFilter 该类必须实现 FilenameFilter 接口
String[] names = file.list(new MyFilenameFilter(".jpg")); // .jpg
for (String name : names) {
System.out.println(name);
}
System.out.println("-----------------");
// 3. 调用 listFiles(FilenameFilter)
File[] files = file.listFiles(new MyFilenameFilter(".java")); // .java
for (File f : files) {
System.out.println(f);
}
}
}
2. FileFilter 文件过滤器接口
package cn.panda.demo3;
import java.io.File;
public class TestFileFilter {
public static void main(String[] args) {
// 需求 : 自定义过滤文件名称的条件, 由外部创建时传入.
// 1. 创建一个 File 对象, 与硬盘的文件夹进行关联
File file = new File("C:/Users/Jack/Desktop/Day10/课程资料");
// 2. 调用 listFiles(FileFilter) 方法
// MyFileFilter 类必须要实现 FileFilter 接口
File[] files = file.listFiles(new MyFileFilter(".mp3"));
// 3. 遍历
for (File f : files) {
System.out.println(f);
}
}
}
package cn.panda.demo3;
import java.io.File;
import java.io.FileFilter;
// 定义一个通用的文件过滤器
public class MyFileFilter implements FileFilter {
// 属性
private String suffix;
// 构造方法传入 suffix
public MyFileFilter(String suffix) {
this.suffix = suffix;
}
@Override
public boolean accept(File pathname) {
// pathname = new File(dir, name);
// System.out.println(pathname);
// 判断
if (pathname.getName().endsWith(suffix)) {
return true;
}
return false;
}
}
说明 : MyFileFilter 类重载了 `构造方法`, 此时, 系统提供的默认无参构造方法就不存在了. 那么该类应该提供一个无参的构造方法吗 ??? 不应该.
因为, 如果该类不提供无参构造方法, 外部创建该类对象时, 必须要指定过滤的 `后缀名`. 因此内部的 suffix 就能够确保一定有值, 使用就不会报错.
如果该类提供了无参构造方法, 外部创建该类对象时, 就可以不指定过滤的 `后缀名`. 这样就会导致 suffix 为 null, 运行时发生 NullPointerException 异常.
19. 递归技术
递归 : 方法自己调用自己.
1. 求 5 的阶乘
package cn.panda.demo4;
public class TestFactorial {
public static void main(String[] args) {
// 需求 : 求 5 的阶乘
// 5 * 4 * 3 * 2 * 1
// 方式一 : 打印精神
System.out.println(5 * 4 * 3 * 2 * 1); // 120
// 方式二 : 循环精神
int sum = 1;
for (int i = 5; i >= 1; i--) {
sum *= i;
}
System.out.println(sum); // 120
// 递归精神 : 方法自己调自己
/*
* 请问 : 5的阶乘是多少 ??? 等于 5 * 4 的阶乘.
* 请问 : 4的阶乘是多少 ??? 等于 4 * 3 的阶乘.
* 请问 : 3的阶乘是多少 ??? 等于 3 * 2 的阶乘.
* 请问 : 2的阶乘是多少 ??? 等于 2 * 1 的阶乘.
* 请问 : 1的阶乘是多少 ??? 等于1.
*
* 思考一 : 如何结束递归 ??? n == 1 return 1;
* 思考二 : 如何继续递归 ??? return n != 1 n * (n-1) 的阶乘.
*/
int factorial = getFactorial(5);
System.out.println(factorial);
}
// 定义一个方法, 求指定数值的递归
public static int getFactorial(int n) {
if (n == 1) {
return 1;
} else {
return n * getFactorial(n-1);
}
}
}
2. 求年龄 :
package cn.panda.demo4;
public class TestAge {
public static void main(String[] args) {
/*
* 有 5 个人, 求第 5 个人的年龄.
*
* 问题 : 第 5 个人多少了 ??? 我比第 4 个人大两岁.
* 问题 : 第 4 个人多少了 ??? 我比第 3 个人大两岁.
* 问题 : 第 3 个人多少了 ??? 我比第 2 个人大两岁.
* 问题 : 第 2 个人多少了 ??? 我比第 1 个人大两岁.
* 问题 : 第 1 个人多少了 ??? 我 10 岁.
*
* 思考一 : 如何结束递归 ??? n == 1 return 10;
* 思考二 : 如果继续递归 ??? n != 1 return 求年龄的方法(n-1) + 2;
*/
int age = getAge(5);
System.out.println(age);
}
public static int getAge(int n) {
if (n == 1) {
return 10;
} else {
return getAge(n-1) + 2;
}
}
}
3. 递归练习 (扫描指定目录中的文件)
单级目录的获取实现 :
package cn.panda.demo5;
import java.io.File;
public class TestPractice {
public static void main(String[] args) {
// 练习 (判断.jpg后缀名的文件)
// 1. 创建一个 File 对象, 与硬盘的文件夹进行关联
File file = new File("C:/Users/Jack/Desktop/Day10/课程资料");
// 2. 使用 file 对象调用 listFiles 方法, 列举文件夹中的所有 `文件和文件夹` 字符串
File[] files = file.listFiles();
// 3. 遍历
for (File f : files) {
if (f.getName().endsWith(".jpg")) {
// .java
System.out.println(f);
}
}
}
}
package cn.panda.demo5;
import java.io.File;
public class TestPractice {
public static void main(String[] args) {
// 练习 (判断.jpg后缀名的文件)
// 1. 创建一个 File 对象, 与硬盘的文件夹进行关联
File file = new File("C:/Users/Jack/Desktop/Day10/课程资料");
// 2. 调用方法
scanFolder(file);
}
private static void scanFolder(File file) {
// 2. 使用 file 对象调用 listFiles 方法, 列举文件夹中的所有 `文件和文件夹` 字符串
File[] files = file.listFiles();
// 3. 遍历
for (File f : files) {
if (f.isDirectory()) {
// 3.1 文件夹, 如果进入文件夹, 或所有的 `文件和文件夹` 数组. (递归调用)
scanFolder(f);
} else {
// 3.2 文件 (判断后缀名)
if (f.getName().endsWith(".jpg")) {
System.out.println(f);
}
}
}
}
}
20. 字节流
File 类只能操作 `文件 / 文件夹`. 但是File 类不可以操作文件中的数据.
IO 流: 使用 `流` 操作文件中的数据.
流总共分类两类 :
1. 字节流 : 可以操作 `所有类型` 的数据. (文本, 图片, 音频, 视频 … 所有数据在计算中都是以字节的形式进行存储的)
2. 字符流 : 只能操作 `字符` 数据. 不应该使用字符流操作 `其它` 数据.
字符流 = 字节流 + 编码表.(ACSII, GBK, UTF-8)
GBK : 一个汉字对应两个字节.
UTF-8 : 一个汉字对应三个字节.
方向 : 以内存作为参照物, 内存流出, 被称为 `输出`, 流入内存, 被称为 `输入`.
1. 输入 : (InputStream / Reader)
2. 输出 : (OutputStream / Writer)
字节流 :
package cn.panda.demo1;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class TestInputStream {
public static void main(String[] args) throws IOException {
// 1. 创建一个字节文件输出流对象
InputStream in = new FileInputStream("demo1.txt");
// 2. 读取 in.read(); 返回的是一个字节的数据
int content = -1;
while ((content = in.read()) != -1) {
System.out.println((char) content);
}
in.close();
}
}
package cn.panda.demo1;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class TestOutputStream {
public static void main(String[] args) throws IOException {
// 1. 创建了一个字节文件输出流对象
OutputStream out = new FileOutputStream("demo1.txt");
// 2. 写入
// write(int b);
out.write(65);
out.write(66);
out.write(67);
out.write(68);
out.write(69);
// 3. 关闭
out.close();
System.out.println("执行完成...");
}
}
字符流 :
1. 输入流read与输出流write方法对比 : 效率差异
package cn.panda.demo1;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class TestReadAndWrite {
public static void main(String[] args) throws IOException {
// 1. 复制
InputStream in = new FileInputStream("demo1.txt");
OutputStream out = new FileOutputStream("demo2.txt");
// 2. 将数据读取到缓冲区, 然后实现写入
byte[] buf = new byte[8];
int len = -1;
while ((len = in.read(buf)) != -1) {
// 写入
// out.write(buf);
out.write(buf, 0, len);
out.flush();
}
// 3. 关闭资源
out.close();
in.close();
/*
System.out.println(Arrays.toString(buf));
int len = in.read(buf);
System.out.println(len);
System.out.println(Arrays.toString(buf));
len = in.read(buf);
System.out.println(len);
System.out.println(Arrays.toString(buf));
len = in.read(buf);
System.out.println(len);
System.out.println(Arrays.toString(buf));
len = in.read(buf);
System.out.println(len);
System.out.println(Arrays.toString(buf));
len = in.read(buf);
System.out.println(len);
*/
}
}
2. 字节输入/输出流实现文件的复制 (缓冲)
实现一 : 一个字节一个字节复制.
package cn.panda.demo1;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestCopyFile {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
// 1.1 创建两个文件对象, 与硬盘的路径进行关联
// 共耗时 : 39088 毫秒.
File src = new File("C:\\Users\\Jack\\Desktop\\Day11\\课程资料\\福利赠送.flv");
File dest = new File("C:\\Users\\Jack\\Desktop\\flzs.flv");
// 1.2 创建两个字节输入 / 输出对象
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest);
// 2. 先读后写
int content = -1;
while ((content = in.read()) != -1) {
// 写入数据
out.write(content);
}
// 3. 关闭资源
out.close();
in.close();
long end = System.currentTimeMillis();
System.out.println("共耗时 : " + (end - start) + " 毫秒.");
}
}
实现二 : 缓冲区数组实现.
package cn.panda.demo1;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestCopyFile2 {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
// 1.1 创建两个文件对象, 与硬盘的路径进行关联
// 共耗时 : 39088 毫秒. / 共耗时 : 56 毫秒. (缓冲区)
File src = new File("C:\\Users\\Jack\\Desktop\\Day11\\课程资料\\福利赠送.flv");
File dest = new File("C:\\Users\\Jack\\Desktop\\flzs.flv");
// 1.2 创建两个字节输入 / 输出对象
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest);
// 2. 先读后写
byte[] buf = new byte[1024]; // 1024的倍数
int len = -1;
while ((len = in.read(buf)) != -1) {
// 写入数据
out.write(buf, 0, len);
}
// 3. 关闭资源
out.close();
in.close();
long end = System.currentTimeMillis();
System.out.println("共耗时 : " + (end - start) + " 毫秒.");
}
}
实现三 : 缓冲流类 + 缓冲区数组共同实现.
package cn.panda.demo1;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestCopyFile2 {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
// 1.1 创建两个文件对象, 与硬盘的路径进行关联
// 共耗时 : 39088 毫秒. / 共耗时 : 56 毫秒. (缓冲区) / 共耗时 : 36 毫秒. (缓冲类 + 缓冲区数组)
File src = new File("C:\\Users\\Jack\\Desktop\\Day11\\课程资料\\福利赠送.flv");
File dest = new File("C:\\Users\\Jack\\Desktop\\flzs.flv");
// 1.2 创建两个字节输入 / 输出对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest));
// 2. 先读后写
byte[] buf = new byte[1024]; // 1024的倍数
int len = -1;
while ((len = bis.read(buf)) != -1) {
// 写入数据
bos.write(buf, 0, len);
}
// 3. 关闭资源
bos.close();
bis.close();
long end = System.currentTimeMillis();
System.out.println("共耗时 : " + (end - start) + " 毫秒.");
}
}
3. 数据的拼接与换行 :
说明 : 将字符串数据转换为 `字节数组`.
关于换行符的使用 : System 类
package cn.panda.demo2;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class TestDetails2 {
public static void main(String[] args) throws IOException {
/*
* 换行 :
* 1. \r\n
* 2. System.lineSeparator();
*/
// 写入数据 (字符串)
// 说明 : 如果文件已经存在, 再次写入数据时, 会造成数据的覆盖.
// OutputStream out = new FileOutputStream("demo3.txt");
OutputStream out = new FileOutputStream("demo3.txt", true);
// 获取系统的换行符
String lineSeparator = System.lineSeparator();
out.write((lineSeparator + "How old are you.").getBytes());
out.write((lineSeparator + "怎么老是你.").getBytes());
out.close();
}
}
4. 数据写入关于父目录的注意点 :
写入数据的注意点 : 如果写入时, 父目录不存在, 写入就会失败.
package cn.panda.demo1;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestCopyFile2 {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
// 1.1 创建两个文件对象, 与硬盘的路径进行关联
// 共耗时 : 39088 毫秒. / 共耗时 : 56 毫秒. (缓冲区) / 共耗时 : 36 毫秒. (缓冲类 + 缓冲区数组)
File src = new File("C:\\Users\\Jack\\Desktop\\Day11\\课程资料\\福利赠送.flv");
// 创建一个文件对象, 与父目录关联
File parentFile = new File("C:\\Users\\Jack\\Desktop\\upload");
// 判断父目录是否存在
if (parentFile.exists() == false) {
// 条件成立, 说明父目录不存在, 需要创建
parentFile.mkdirs();
}
File dest = new File(parentFile, "flzs.flv");
// 1.2 创建两个字节输入 / 输出对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest));
// 2. 先读后写
byte[] buf = new byte[1024]; // 1024的倍数
int len = -1;
while ((len = bis.read(buf)) != -1) {
// 写入数据
bos.write(buf, 0, len);
}
// 3. 关闭资源
bos.close();
bis.close();
long end = System.currentTimeMillis();
System.out.println("共耗时 : " + (end - start) + " 毫秒.");
}
}
5. 练习一 : 复制目录及子目录 (多层复制)
package cn.panda.demo3;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestCopyFolder {
public static void main(String[] args) throws IOException {
// 1. 创建两个File对象, 一个与 `源文件夹` 关联, 另一个与 `目的地文件夹` 关联
File srcFile = new File("C:\\Users\\Jack\\Desktop\\Day11\\课程资料");
File destFile = new File("C:\\Users\\Jack\\Desktop");
// 2. 调用方法, 实现文件夹复制
copyFolder(srcFile, destFile);
}
private static void copyFolder(File srcFile, File destFile) throws IOException {
// 2. 在目的地位置创建一个与 `源文件夹` 名称相同的文件夹
String name = srcFile.getName();
// 3. 使用 `目的地文件对象 + 源文件名称对象` 拼接出需要创建的文件夹对象
// C:\Users\Jack\Desktop\课程资料
File folder = new File(destFile, name);
// 4. 先判断, 如果不存在该文件夹, 就创建
if (folder.exists() == false) {
folder.mkdirs();
}
// 5. 进入到 `源文件夹` 内部, 获取所有的 `文件和文件夹对象` 数组.
File[] files = srcFile.listFiles();
// 6. 循环遍历该数组, 并判断.
for (File f : files) {
if (f.isDirectory()) {
// 6.1 文件夹 (继续创建, 文件夹不可以复制)
// System.out.println(f + " -> 是文件夹, 需要再次创建该文件夹 ...");
/*
* 源数据 : C:\Users\Jack\Desktop\Day11\课程资料\folders (f对象)
* 目的地 : C:\Users\Jack\Desktop\课程资料 (folder对象)
*/
copyFolder(f, folder);
} else {
// 6.2 文件 (实现复制)
// System.out.println(f + " 需要复制.");
/***************** 文件复制 start *******************/
// 1. 创建两个高效的缓冲字节输入/输出流
/*
* 源数据 : C:\Users\Jack\Desktop\Day11\课程资料\1.jpg
* 目的地 : C:\Users\Jack\Desktop\课程资料 (folder对象) \1.jpg (f.getName()源文件的名称);
*/
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(f));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(
new File(folder, f.getName())));
// 2. 先读后写
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
// 写入
bos.write(buf, 0, len);
bos.flush();
}
// 3. 关闭资源
bos.close();
bis.close();
/***************** 文件复制 end *******************/
}
}
}
}
6. 练习二 : 删除文件夹
package cn.panda.demo3;
import java.io.File;
public class TestDelete {
public static void main(String[] args) {
File file = new File("C:\\Users\\Jack\\Desktop\\课程资料");
// 非空文件夹无法删除
// boolean delete = file.delete();
// System.out.println(delete);
// 调用方法, 删除文件夹
deleteFolder(file);
}
private static void deleteFolder(File file) {
// 1. 列出文件夹中的所有 `文件/文件夹` 对象
File[] files = file.listFiles();
// 2. 遍历
for (File f : files) {
// 3. 判断
if (f.isDirectory()) {
// 文件夹
// System.out.println(f + " 是文件夹, 需要再次进入 ...");
deleteFolder(f);
} else {
// 文件
f.delete();
}
}
// 4. for 循环外部删除文件夹
file.delete();
}
}
21. 字符流
字符流 = 字节流 + 编码表
请问 : 为什么要出现字符流 ??? 因为字节流读取 `字符` 数据有问题.
编码表 :
1. ACSII (128个字符)
2. ISO-8859-1 (255个字符)
3. Unicode (计算机协会编码表) Java语言默认集成 Unicode 编码.
主流编码表 :
1. GBK (GB2312 中国的编码表, 只能识别6~7千个汉字) 识别 2~3 万个. 是汉字主流的编码表.
特点 : 使用二个字节表示一个汉字. 所有编码表都会集成 ASCII 码表.
2. UTF-8 (Unicode编码的升级版, 非常智能)
特点 :
1. 一个字节能表示的, 就使用一个字节. (ASCII)
2. 二个字节能表示的, 就使用二个字节. (识别很多中语言)
3. 三个字节能表示的, 就使用三个字节. (使用三个字节来表示一个汉字)
掌握 : GBK, UTF-8
package cn.panda.demo4;
import java.io.FileOutputStream;
import java.io.IOException;
public class TestOutputStream {
public static void main(String[] args) throws IOException {
FileOutputStream out = new FileOutputStream("demo4.txt");
// 问题 : 因为字节流不能写入 `字符串` 数据, 需要将字符串转换为 `字节数组`.
out.write("今天天气不错, 心情也不错.".getBytes());
out.close();
}
}
package cn.panda.demo4;
import java.io.FileInputStream;
import java.io.IOException;
public class TestInputStream {
public static void main(String[] args) throws IOException {
FileInputStream in = new FileInputStream("demo4.txt");
int content = -1;
while ((content = in.read()) != -1) {
System.out.print((char) content);
}
in.close();
}
}
分析 : 字节流读取数据是 `一个字节一个字节` 读取的. 可是中文不是一个字节进行编码的.
字母刚好是一个字节一个字母存储的.
1. FileWriter 和 FileReader 便捷流 :
用来读取字符文件的便捷类。此类的构造方法假定默认字符编码和默认字节缓冲区大小都是适当的。
package cn.panda.demo4;
import java.io.FileReader;
import java.io.IOException;
public class TestFileReader {
public static void main(String[] args) throws IOException {
FileReader reader = new FileReader("demo4.txt");
int content = -1;
while ((content = reader.read()) != -1) {
System.out.print((char) content);
}
reader.close();
}
}
问题 : FileReader 便捷类的 read 方法, 到底是如何读取数据呢 ??? 根据编码表的规则进行数据的读取.
FileWriter 便捷类可以直接写入字符串数据, 无需将字符串转换 `字节数组`.
便捷类的原因 : 没有指定编码表. (底层 实现 默认编码表)
2. OutputStreamWriter 和 InputStreamReader 转换流 :
OutputStreamWriter 是字符流通向字节流的桥梁.
问题1 : OutputStreamWriter 类会返回一个 `字符流` 对象.
问题2 : 创建该类时, 需要传入什么对象 ??? 需要传入一个 `字节流` 对象.
// 需求 : 使用 UTF-8 编码表写入数据 字符流对象 = 字节流对象 + 编码表
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("demo5_utf8.txt"), "UTF-8");
FileWriter 是 OutputStreamWriter 的子类.
FileWriter = new OutputStreamWriter(new FileOutputStream("demo5_utf8.txt"), 默认编码表);
package cn.panda.demo5;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
public class TestOutputStreamWriter {
public static void main(String[] args) throws IOException {
// 需求 : 使用 UTF-8 编码表写入数据
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("demo5_utf8.txt"), "UTF-8");
writer.write("大家好, 我是渣渣辉, 今年18岁, 未婚, 男");
writer.close();
}
}
InputStreamReader 是字节流通向字符流的桥梁.
FileReader = 父类 + 默认编码表
package cn.panda.demo5;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class TestInputStreamReader {
public static void main(String[] args) throws IOException {
// A编码表进行数据 `编码`, B编码表进行 `解码`, 结果就是乱码.
// 1. 编码 : 将字符串编译为 `01010101010` 字节数据.
// 2. 解码 : 将 `01010101010` 字节数据通过指定编码表翻译为字符串数据.
// 字符流 = 字节流 + 编码表
InputStreamReader reader = new InputStreamReader(new FileInputStream("demo5_utf8.txt"), "utf8");
// 三个字节 : 111xxxxx 11xxxxxx 1xxxxxxx
// 二个字节 : 11xxxxxx 1xxxxxxx
// 一个字节 : 1xxxxxxx
int content = -1;
while ((content = reader.read()) != -1) {
System.out.print((char) content);
}
reader.close();
}
}
3. Reader & Writer 字符流复制文本文件 : (了解)
说明 : 选择字符流的原因 ???
1. 写入 `汉字 / 字符串` 数据.
2. 将数据读取到程序中, 并且需要查看. 而且还要看得懂.
如果是复制操作, 无论复制是什么类型数据, 都应该选择 `字节流`.
public class TestCopyFile {
public static void main(String[] args) throws IOException {
// 需求 : 字符流实现文件复制
// 1. 创建两个字符流对象
FileReader reader = new FileReader("C:\\Users\\Jack\\Desktop\\Day11\\课程资料\\柳岩.jpg");
FileWriter writer = new FileWriter("C:\\Users\\Jack\\Desktop\\ly2.jpg");
// 2. 读写操作
char[] charBuf = new char[1024];
int len = -1;
while ((len = reader.read(charBuf)) != -1) {
writer.write(charBuf, 0, len);
}
// 3. 关闭资源
writer.close();
reader.close();
}
}
4. BufferedReader & BufferedWriter字符缓冲流 :
正确的姿势 : 一行一行写入数据, 一行一行读取数据.
说明 : BufferedReader 类接收一个 Reader 对象, 我们可以传入哪些对象 ???
1. FileReader 对象. (默认编码表)
2. InputStreamReader 对象. (UTF-8编码表)
读取默认编码表的数据 :
BufferedReader reader = new BufferedReader(new FileReader("demo3.txt"));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
读取 UTF-8 编码表的数据 :
/*
FileInputStream in = new FileInputStream("demo5_utf8.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
*/
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream("demo5_utf8.txt"), "UTF-8"));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
请问 : 如何传入 Writer 对象 ???
1. FileWriter : 默认编码表.
2. OutputStreamWriter : UTF-8编码表
写入默认编码表的数据 : (写入, 换行, 刷新)
BufferedWriter writer = new BufferedWriter(new FileWriter("demo7_gbk.txt"));
writer.write("What a beautiful life.");
writer.newLine();
writer.flush();
writer.write("这才叫生活啊.");
writer.newLine();
writer.flush();
writer.close();
写入 UTF-8 编码表的数据 :
FileOutputStream out = new FileOutputStream("demo8_utf8.txt");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
writer.write("What a beautiful life.");
writer.newLine();
writer.flush();
writer.write("这才叫生活啊.");
writer.newLine();
writer.flush();
writer.close();
5. 字节流与字符流总结 :
/*
* IO 流共分为两大类 :
*
* 1. 字节流 作用: 复制 (上传, 下载 -> 所有类型的数据)
*
* 读: BufferedInputStream 传递 InputStream (FileInputStream)
* 写: BufferedOutputStream 传递 OutputStream (FileOutputStream)
*
* 2. 字符流 作用: 读字符串或汉字, 要看懂. 写字符串或汉字, 需要编码.
*
* 读: BufferedReader 传递 (FileReader, InputStreamReader)
* 1. FileReader -> 传递 File / String
* 2. InputStreamReader -> 传递 InputStream (FileInputStream)
*
* 写: BufferedWriter 传递 (FileWriter, OutputStreamWriter)
* 1. FileWriter -> 传递 File / String
* 2. OutputStreamWriter -> 传递 OutputStream (FileOutputStream)
*/
22. 序列化 & 反序列化 : (了解)
序列化 : 将 Java 对象写入到文件中.
ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化。
反序列化 : 将文件中的数据读取到内存的程序中.
注意点 :
1. 对象所属的类型必须要实现一个接口. (Serializable接口)
2. 对象所属的类需要提供一个 `序列版本号`.
/**
* 默认序列版本号 : 1L
*/
private static final long serialVersionUID = 1L;
/**
* 自动生成序列版本号 :
*/
private static final long serialVersionUID = 3731195116443445031L;
不能序列化异常.
非法类异常.
类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。
说明 : 没有任何方法的接口被称为 `标示性` 接口.
package cn.panda.demo1;
import java.io.Serializable;
public class Student implements Serializable {
/**
* 自动生成序列版本号 :
*/
private static final long serialVersionUID = 3731195116443445031L;
// 关键字 : transient 瞬态的 (被 transient 修饰的属性, 不参与序列化.)
private String name;
private transient int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public Student() {
super();
}
public void show() {
System.out.println(age + "岁的" + name + "正在表演中 ...");
}
public void sing() {
System.out.println(age + "岁的" + name + "正在唱歌中 ...");
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
序列化代码实现 :
package cn.panda.demo1;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class TestObjectOutputStream {
public static void main(String[] args) throws IOException {
Student stu = new Student("柳岩", 18);
// 1. 创建一个 `对象输出流 ` (序列化)
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("stu.txt"));
// 2. 写入对象到文件中
oos.writeObject(stu);
// 3. 关闭资源
oos.close();
}
}
反序列化代码实现 :
package cn.panda.demo1;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class TestObjectInputStream {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 反序列化 : 将文件中的数据, 再次读取为 Java 对象
// 1. 创建一个对象读取流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("stu.txt"));
// 2. 读取对象
Object obj = ois.readObject();
System.out.println(obj);
// 3. 关闭资源
ois.close();
}
}
23. properties 属性集类 (重点)
第一步 : 创建一个属性集对象.
第二步 : 将配置文件中的数据加载到 prop 对象中.
第三步 : 使用 getProperty 方法, 传入 key 获取 value.
作用 : 作为程序的 `配置文件`.
Properties
类表示了一个持久的属性集。Properties
可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串。
package cn.panda.demo2;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class TestProperties {
public static void main(String[] args) throws IOException {
// 属性集类 : 读取配置文件 (.properties 后缀名)的信息.
// 1. 创建一个 Properties 属性集对象
Properties prop = new Properties();
// 2. 加载数据到 属性集 对象中
prop.load(new FileReader("user.properties"));
// 3. 使用 getProperty 获取对应的 value
String username = prop.getProperty("username");
String password = prop.getProperty("password");
System.out.println(username + " = " + password);
}
}
遍历属性集对象中的所有信息 :
package cn.panda.demo2;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;
public class TestProperties2 {
public static void main(String[] args) throws IOException {
// 1. 创建一个属性集对象
Properties prop = new Properties();
// 2. 加载数据到 prop 对象中
prop.load(new FileReader("四大天王.properties"));
// 3. 获取属性集对象中的所有 `字符串属性名称集合`
Set<String> keys = prop.stringPropertyNames();
for (String key : keys) {
// 4. 使用 getProperty 获取
String value = prop.getProperty(key);
System.out.println(key + " = " + value);
}
}
}
Properties 文件的格式 : key = value
了解 : 将信息使用 Properties 对象存储到文件中.
package cn.panda.demo2;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
public class TestProperties3 {
public static void main(String[] args) throws IOException {
// 1. 创建一个 Properties 对象
Properties prop = new Properties();
// 2. 设置数据
prop.setProperty("沉鱼", "西施");
prop.setProperty("落雁", "王昭君");
prop.setProperty("闭月", "貂蝉");
prop.setProperty("羞花", "杨玉环");
// 3. 存储数据到文件中
// comments 评论 / 注释
prop.store(new FileWriter("四大美女.properties"), "four big beauty girls.");
}
}
24. 打印流 (了解)
特点 : 自动刷新.
特点 : 手动启动自动刷新.
说明 : 流对象在 close() 之前会进行数据的刷新.
需求 : 实现打印数据到文件中.
// printStream 类的 println 方法.
System.out.println("hello world.");
字节打印流 :
package cn.panda.demo2;
import java.io.IOException;
import java.io.PrintStream;
public class TestPrintStream {
public static void main(String[] args) throws IOException {
// 字节输出流
// 1. 创建一个打印输出流 (自动刷新)
PrintStream stream = new PrintStream("stream.txt");
// 2. 一行一行输出
stream.println("How are you ?");
stream.println("I am fine, Thank you, and you ?");
stream.println("I am fine too, byebye.");
// 3. 关闭资源
stream.close();
}
}
字符打印流 :
package cn.panda.demo2;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
public class TestPrintWriter {
public static void main(String[] args) throws IOException {
// 字节输出流
// 1. 创建一个打印输出流 (手动刷新)
// PrintWriter writer = new PrintWriter("writer.txt");
PrintWriter writer = new PrintWriter(new FileOutputStream("writer.txt"), true);
// 2. 一行一行输出
writer.println("How are you ?");
writer.println("I am fine, Thank you, and you ?");
writer.println("I am fine too, byebye.");
// 3. 关闭资源
writer.close();
}
}
25. Commons-IO 工具类的使用 : (了解)
通用 IO 工具类 : 是 Apache 软件基金组织提供的工具包.
使用第三方提供的工具类步骤 :
1. 在当前项目下创建一个文件夹. 命名为 lib (library库)
2. 将 jar 包 (Java档案包, class文件压缩包) 导入到项目中.
3. 将 jar 添加到 `编译路径 add to build path` 下.
package cn.panda.demo3;
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
public class TestCommonsIO {
public static void main(String[] args) throws IOException {
// 复制文件 :
File srcFile = new File("C:\\Users\\Jack\\Pictures\\Saved Pictures\\7.jpg");
File destFile = new File("C:\\Users\\Jack\\Desktop\\meinv.jpg");
FileUtils.copyFile(srcFile, destFile);
// 复制文件夹 :
File srcDir = new File("C:\\Users\\Jack\\Pictures\\Saved Pictures");
File destDir = new File("C:\\Users\\Jack\\Desktop");
FileUtils.copyDirectoryToDirectory(srcDir, destDir);
// 删除文件夹 :
FileUtils.deleteDirectory(new File("C:\\Users\\Jack\\Desktop\\Saved Pictures"));
System.out.println("执行完成...");
}
}
26. 面向对象,字符串,集合,IO综合练习 :
练习一 :
编程题一 : 编写一个程序, 完成键盘录入员工信息,并计算员工到手薪资,将所有信息写入到文本文件中存储.
需求:键盘录入3个员工信息(姓名,薪资,扣税,到手薪资)
求出每个员工的到手薪资. 到手薪资 = 薪资 - 扣税.
并且将员工的信息写入employee.txt文件中.
效果下所示 :
姓名 薪资 扣税 到手薪资
张三 15000 3000 12000
李四 16000 3200 12800
王五 17000 3500 13500
代码实现 :
package cn.panda.demo3;
public class Employee {
private String name;
private int salary;
private int tax;
private int money;
public Employee(String name, int salary, int tax, int money) {
this.name = name;
this.salary = salary;
this.tax = tax;
this.money = money;
}
public Employee() {
}
@Override
public String toString() {
return "Employee [name=" + name + ", salary=" + salary + ", tax=" + tax + ", money=" + money + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getTax() {
return tax;
}
public void setTax(int tax) {
this.tax = tax;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
}
package cn.panda.demo3;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;
public class TestPractice1 {
public static void main(String[] args) throws IOException {
// 1. 键盘录入
Scanner sc = new Scanner(System.in);
// 6.1 定义一个用于存储Employee对象的集合
ArrayList<Employee> list = new ArrayList<Employee>();
// 2. 循环接收 3 次用户输入
for (int i = 1; i <= 3; i++) {
// 3. 提示, 并接收用户的输入
System.out.println("亲, 请输入第"+i+"个员工的姓名 :");
String name = sc.nextLine();
System.out.println("亲, 请输入第"+i+"个员工的薪资 :");
int salary = Integer.parseInt(sc.nextLine());
System.out.println("亲, 请输入第"+i+"个员工的扣税 :");
int tax = Integer.parseInt(sc.nextLine());
// 4. 计算到手金额
int money = salary - tax;
// 5. 将用户输入的数据封装为一个 `Employee` 对象
Employee emp = new Employee(name, salary, tax, money);
// 6.2 将封装完成数据的员工对象存储到集合中
list.add(emp);
}
// 7. 写员工中的所有信息写入到文件
writeListToFile(list);
sc.close();
}
// 定义一个方法 :
private static void writeListToFile(ArrayList<Employee> list) throws IOException {
// 1.1 定义一个字符串缓冲区
StringBuilder sb = new StringBuilder();
sb.append("姓名\t薪资\t\t扣税\t\t到手薪资").append(System.lineSeparator());
// 2. 遍历集合
for (Employee emp : list) {
// 3. 将 emp 对象的数据拼接到 `字符串缓冲区中`
sb.append(emp.getName()).append("\t").append(emp.getSalary()).append("\t")
.append(emp.getTax()).append("\t").append(emp.getMoney())
.append(System.lineSeparator());
}
// 4. 执行写入
// 4.1 创建一个高效的字符缓冲输出流
BufferedWriter writer = new BufferedWriter(new FileWriter("employee.txt"));
// 4.2 写入
writer.write(sb.toString());
writer.flush();
// 4.3 关闭资源
writer.close();
}
}
练习二 :
编程题二 : 在项目根目录下已存在文件student.txt,文件中存放学生姓名与总成绩.
编写程序, 将数据读取到内存中, 并根据学生的名称, 输出该学生对应的总成绩信息.
学员,总成绩(中间用,分隔)
小明,180
小花,190
小丽,175
小草,120
小罗,210
小蒋,200
代码实现 :
package cn.panda.demo3;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Scanner;
public class TestPractice2 {
public static void main(String[] args) throws IOException {
// 1. 创建一个文件输入流
BufferedReader reader = new BufferedReader(new FileReader("students.txt"));
// 2.4 创建一个映射表集合
HashMap<String, Integer> map = new HashMap<String, Integer>();
// 2. 一行一行读取数据
String line = null;
while ((line = reader.readLine()) != null) {
// System.out.println(line);
// 2.2 切割读取的 line 数据
String[] datas = line.split(",");
// 2.3 取出切割后的数据
String name = datas[0];
int score = Integer.parseInt(datas[1]);
// 2.5 将截取的数据存储到 map 集合中.
map.put(name, score);
}
// 3. 关闭资源
reader.close();
// 4. 提示, 并接收用户输入的查询名称
Scanner sc = new Scanner(System.in);
System.out.println("亲, 请输入需要查询成绩的学生名称 :");
String name = sc.nextLine();
sc.close();
// 5. 根据名称, 去映射表中查询对应的学生成绩
Integer score = map.get(name);
// 6. 判断
if (score == null) {
System.out.println("亲, 没有该学生对应的成绩信息.");
} else {
System.out.println(score);
}
}
}
27. 多线程
思考 : 能否在同一个Java应用程序中执行2个以上的无限循环呢?
线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。 (一个程序可以允许同时执行多条路径, 开辟多条线程)
创建新执行线程有两种方法。
第一种方法是将类声明为 Thread
的子类。该子类应重写 Thread
类的 run
方法。接下来可以分配并启动该子类的实例。
package cn.panda.demo5;
public class HelloThread extends Thread {
@Override
public void run() {
// 第一个耗时操作, 放在一个独立的执行路径中 ...
while (true) {
System.out.println("hello ...");
}
}
}
package cn.panda.demo5;
public class WorldThread extends Thread {
@Override
public void run() {
// 第二个耗时操作, 放在一个独立的执行路径中 ...
while (true) {
System.out.println("world ...");
}
}
}
然后,下列代码会创建并启动一个线程:
package cn.panda.demo5;
public class TestDeadLoop {
public static void main(String[] args) {
// 多线程产生的效果 : 类似于 `乱序` 执行. 可以完成多个耗时操作同时执行.
// 第一个耗时操作, 放在一个独立的执行路径中 ...
HelloThread t1 = new HelloThread();
t1.start();
// 第二个耗时操作, 放在一个独立的执行路径中 ...
WorldThread t2 = new WorldThread();
t2.start();
// 第三个耗时操作, 放在 `主线程` 执行路径中 ...
while (true) {
System.out.println("main ...");
}
}
}
总结第一种多线程实现方案的步骤 :
1. 创建一个子类, 继承 Thread 类, 该类就被称为 `线程类`.
2. 子类需要重写 Thread 类的 run() 方法.
3. 创建一个子类对象.
4. 使用子类对象调用父类中的 start() 方法.
package cn.panda.demo5;
// 1. 定义一个子类, 继承自 Thread, 该类就可以被称为 `线程类`.
public class TestThread extends Thread {
// 构造方法, 设置线程的名称
public TestThread(String name) {
super(name);
}
public TestThread() {
}
// 2. 子类需要重写父类中的 run() 方法.
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// Thread 类中已经提供了 name 属性. (私有的)
System.out.println(getName() + " run -> ... " + i);
}
}
}
package cn.panda.demo5;
/*
* 说明 : 每一条线程都有名称 :
*
* 子线程的默认名称 : Thread-n n从0开始, 依次递增.
* 1. 调用 Thread(String) 构造方法设置.
* 2. 使用线程对象调用 setName(String) 也可以设置.
*
* 获取线程的名称 :
* 1. 父子关系. getName();
* 2. 无父子关系. Thread.currentThread().getName()
*/
public class TestMainThread {
public static void main(String[] args) {
// 3. 创建一个线程类对象
TestThread t1 = new TestThread("one");
// 4. 使用线程类对象调用 start() 方法, 启动开辟的新线程
t1.start();
TestThread t2 = new TestThread();
t2.setName("two");
t2.start();
// 主线程的执行代码...
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " run -> ... " + i);
}
}
}
创建线程的另一种方法是声明实现 Runnable
接口的类。该类然后实现 run
方法。然后可以分配该类的实例,在创建 Thread
时作为一个参数来传递并启动。
说明 : Thread 类实现了 Runnable 接口, run() 方法为任务类代码, Thread 是线程类代码. (Thread类将任何代码和线程代码进行紧密的关联)
独立执行的线程指的就是 `新开辟` 的线程. (在run方法中编写的代码会在新开辟的线程中被执行.)
实现Runnable 接口完成多线程的步骤 :
1. 定义一个类, 实现 Runnable 接口. 该类就可以被称为 `任务类`.
2. 实现类要重写 Runnable 接口的 run() 方法.
3. 创建一个 `任务类` 对象.
4. 创建一个 `线程类` 对象, 并同时将任务类对象作为参数传入到 Thread 类的构造方法中.
5. 使用 `线程类` 对象调用 start 方法, 启动新线程.
package cn.panda.demo6;
// 1. 定义一个类,实现 Runnable 接口
public class RunnableTask implements Runnable {
// 2. 该实现类重写 Runnable 接口中的 run 抽象方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " -> run ... " + i);
}
}
}
package cn.panda.demo6;
public class TestRunnable {
public static void main(String[] args) {
// 3. 创建一个 `任务类` 对象
RunnableTask task = new RunnableTask();
// 4. 创建一个线程类对象, 同时要将任务类对象作为参数传入
// 说明 : 两条线程执行的是同一个任务, 因此只需要创建一次 `任务类对象` 即可
Thread t1 = new Thread(task, "线程一");
Thread t2 = new Thread(task, "线程二");
// 5. 启动新线程
t1.start();
t2.start();
// 主线程执行代码...
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " -> run ... " + i);
}
}
}
1. 两种实现多线程方式的对比分析
/*
请问 : 既然直接继承Thread类和实现Runnable接口都能实现多线程, 那么这两种实现多线程的方式在实际应用中又有什么区别呢 ?
需求 : 假设某航空公司有三个窗口发售某日某次航班的100张票,这时,100张票可以作为共享资源,三个售票窗口需要创建三个线程. 使用代码模拟实现.
注意 : 不是每个售票点都卖 100 张票,是一共卖了 100 张票.
*/
1.1 继承Thread类方案实现 :
package cn.panda.demo7;
// 1. 定义一个售票窗口类, 继承自 Thread 类.
public class TicketWindowThread extends Thread {
// 属性 (共享资源)
// 说明 : 无论多少个售票窗口, 卖的都是这里的同 100 张票.
private int tickets = 100;
// 2. 子类重写父类的 run() 方法
@Override
public void run() {
// 循环卖票
while (true) {
// 判断
if (tickets > 0) {
// 有票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
// 票数自减
tickets--;
} else {
// 无票
break;
}
}
}
}
package cn.panda.demo7;
public class TestWindowThread {
public static void main(String[] args) {
// 3. 创建一个 `线程类` 对象 (模拟3个售票窗口)
TicketWindowThread t1 = new TicketWindowThread();
t1.setName("窗口一");
TicketWindowThread t2 = new TicketWindowThread();
t2.setName("窗口二");
TicketWindowThread t3 = new TicketWindowThread();
t3.setName("窗口三");
// 4. 启动线程
t1.start();
t2.start();
t3.start();
}
}
1.2 实现Runnable接口方案实现 :
package cn.panda.demo7;
// 1. 定义一个类, 实现 Runnable 接口
public class TicketWindowTask implements Runnable {
// 属性
private int tickets = 100;
// 2. 重写 Runnable 接口的 run 抽象方法
@Override
public void run() {
// 循环卖票
while (true) {
// 判断
if (tickets > 0) {
// 有票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
// 票数自减
tickets--;
} else {
// 无票
break;
}
}
}
}
package cn.panda.demo7;
public class TestTicketWindowTask {
public static void main(String[] args) {
// 3. 创建一个 `任务类` 对象 (任务就是买票)
TicketWindowTask task = new TicketWindowTask();
// 4. 创建三个线程对象, 模拟三个售票窗口
Thread t1 = new Thread(task, "窗口一");
Thread t2 = new Thread(task, "窗口二");
Thread t3 = new Thread(task, "窗口三");
// 5. 启动线程
t1.start();
t2.start();
t3.start();
}
}
1.3 两种方案实现的对比 :
1. 接口方案实现多线程, 共享数据更加的方便. 只要是同一个任务, 数据就仅有一份.
2. 接口方案实现多线程, 类和类之间耦合度更低. 因为任务代码和线程代码完全分离, 而继承方案, 将任务代码与线程紧密关联.因为继承方案创建出来的对象即使线程对象, 同时也表示任务对象, 因为任务代码就是在子类中重写的.
3. Java语言是 `单继承`. 请问, 我们设计一个类肯定优先考虑继承. 如果后期该类出现 `耗时操作`, 将这个耗时操作放到子线程执行时, 此时, 我们只能选择接口方案.
2. 线程同步
目的 : 解决多线程中共享资源的数据安全问题.
2.1 方案一 : 同步代码块
synchronized (锁对象) {
// 多线程操作 / 修改共享资源的代码 …
}
注意点1 : 锁对象是什么 ??? 任意类型的对象. (Object, String, Student …) 如果使用 Runnable 实现多线程, 锁对象可以使用 this 对象.
注意点2 : 锁对象必须保证唯一性.
package cn.panda.demo2;
public class WindowTicketTask implements Runnable {
// 属性
private int tickets = 100;
// 定义一个锁对象
// 当前任务对象仅被创建过一次, 因此总共就出现一把锁.
Object lock = new String();
@Override
public void run() {
// 三条线程都是执行 run() 方法, 因此就会产生 3 把锁.
// Object lock = new Object();
// 循环买票
while (true) {
// 判断
// 方案一 : 同步代码块
synchronized (lock) {
if (tickets > 0) {
// 演示 : 强制让 CPU 实现线程间的切换 Thread.sleep();
// 假设 : 有三个售票窗口. 如果窗口一执行到该代码, 如何让窗口一停止运行, 目的就是为了让 CPU 切到窗口二 / 三中.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
tickets--;
} else {
break;
}
}
}
}
}
package cn.panda.demo2;
public class TestWindowTicket {
public static void main(String[] args) {
// 1. 创建一个 `任务类` 对象
WindowTicketTask task = new WindowTicketTask();
// 2. 模拟三个售票窗口
Thread t1 = new Thread(task, "窗口一");
Thread t2 = new Thread(task, "窗口二");
Thread t3 = new Thread(task, "窗口三");
// 3. 启动线程
t1.start();
t2.start();
t3.start();
}
}
2.1 方案二 : 同步方法 (重点)
package cn.panda.demo2;
public class WindowTicketTask2 implements Runnable {
// 属性
private int tickets = 100;
// 定义一个锁对象
// 当前任务对象仅被创建过一次, 因此总共就出现一把锁.
Object lock = new String();
@Override
public void run() {
// 循环买票
while (true) {
// 判断
this.sellTickets();
if (tickets <= 0) {
break;
}
}
}
// 定义一个 `同步方法 `
// 请问 : 同步方法有没有锁 ??? 有, 谁是同步方法的锁对象 ??? this 对象. (当前任务类对象)
private synchronized void sellTickets() {
if (tickets > 0) { //防止最后卖到-1/-2张
// 演示 : 强制让 CPU 实现线程间的切换 Thread.sleep();
// 假设 : 有三个售票窗口. 如果窗口一执行到该代码, 如何让窗口一停止运行, 目的就是为了让 CPU 切到窗口二 / 三中.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
tickets--;
}
}
}
2.1 方案三 : 静态同步方法
静态方法没有隐式的 this 对象.
package cn.panda.demo2;
public class WindowTicketTask3 implements Runnable {
// 属性
private static int tickets = 100;
// 定义一个锁对象
// 当前任务对象仅被创建过一次, 因此总共就出现一把锁.
Object lock = new String();
@Override
public void run() {
// 循环买票
while (true) {
// 判断
WindowTicketTask3.sellTickets();
if (tickets <= 0) {
break;
}
}
}
// 定义一个 `静态同步方法 `
// 请问 : 静态同步方法有没有 this 对象, 那么静态还有锁吗 ??? 有. 是谁 ??? 是当前类的 Class 对象. (反射)
private static synchronized void sellTickets() {
// 同一个类只有一个 Class 对象. 绝对唯一.
// Class<?> cls = WindowTicketTask3.class;
if (tickets > 0) {
// 演示 : 强制让 CPU 实现线程间的切换 Thread.sleep();
// 假设 : 有三个售票窗口. 如果窗口一执行到该代码, 如何让窗口一停止运行, 目的就是为了让 CPU 切到窗口二 / 三中.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
tickets--;
}
}
}
2.1 方案四 : lock & ReentrantLock
接口 Lock & ReentrantLock 实现类.
一个可重入的互斥锁 Lock
,它具有与使用 synchronized
方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
方法一 : 上锁 lock();
方法二 : 解锁 unlock();
方案一 : 使用两个 lock.unlock(); 解决
package cn.panda.demo2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class WindowTicketTask4 implements Runnable {
// 属性
private int tickets = 100;
// 定有一个 Lock 接口的实现类对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
// 循环买票
while (true) {
// t1 t2
// 获取锁
lock.lock();
// 判断
if (tickets > 0) { // 0 张
// 演示 : 强制让 CPU 实现线程间的切换 Thread.sleep();
// 假设 : 有三个售票窗口. 如果窗口一执行到该代码, 如何让窗口一停止运行, 目的就是为了让 CPU 切到窗口二 / 三中.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
tickets--;
} else {
lock.unlock();
break;
}
// 释放锁
lock.unlock();
}
}
}
以上这个问题仅出现在 while 循环 + break 中, 主要原因就是因为 break 之前没有执行 lock.unlock(); 代码.
方案二 : 同步方法 :
public class WindowTicketTask5 implements Runnable {
// 属性
private int tickets = 100;
// 定有一个 Lock 接口的实现类对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
// 循环买票
while (true) {
sellTickets();
if (tickets <= 0) {
break;
}
}
}
private void sellTickets() {
// 获取锁
lock.lock();
// 判断
if (tickets > 0) {
// 演示 : 强制让 CPU 实现线程间的切换 Thread.sleep();
// 假设 : 有三个售票窗口. 如果窗口一执行到该代码, 如何让窗口一停止运行, 目的就是为了让 CPU 切到窗口二 / 三中.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
tickets--;
}
// 释放锁
lock.unlock();
}
}
方案三 : try – finally 解决 :
package cn.panda.demo2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class WindowTicketTask4 implements Runnable {
// 属性
private int tickets = 100;
// 定有一个 Lock 接口的实现类对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
// 循环买票
while (true) {
try {
// 获取锁
lock.lock();
// 判断
if (tickets > 0) {
// 演示 : 强制让 CPU 实现线程间的切换 Thread.sleep();
// 假设 : 有三个售票窗口. 如果窗口一执行到该代码, 如何让窗口一停止运行, 目的就是为了让 CPU 切到窗口二 / 三中.
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行卖票
System.out.println(Thread.currentThread().getName() + "正在售出第" + tickets + "张票.");
tickets--;
} else {
break;
}
} finally {
// 释放锁
lock.unlock();
}
}
}
}
3. 匿名内部类实现多线程
观察 : 匿名子类重写的代码在 {} 大括号中.
// 1. 匿名子类对象
// new Thread(){}; new Object(){};
new Thread(){
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("匿名子类实现多线程 -> run ... " + i);
}
}
}.start();
观察 : 匿名实现类的代码在 () 小括号中.
// 2. 匿名实现类
// new Thread(Runnable target); 构造方法
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("匿名实现类 -> run ... " + i);
}
}
}).start();
变态版的面试题 :
package cn.panda.demo2;
public class TestAnonymous {
public static void main(String[] args) {
// 匿名对象完成多线程. (匿名子类对象 / 匿名实现类对象)
// 面试题 : 执行谁 ??? 为什么 ???
// 子类继承父类, 如果子类重写了父类的方法, 程序就会执行子类重写的方法.
new Thread(new Runnable(){
// Runnable 实现类对象最终传递给了 Thread 类的 target 属性, Thread 类在自己的 run() 方法中调用了 target.run() 方法.
// 因此, 说明了. 如果 Thread 的 run() 能够被执行, 实现类的 run() 就会被调用.
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("匿名实现类 -> run ... " + i);
}
}
}){
// 子类重写了父类的方法, 程序就会执行子类重写的方法. 抽象类
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("匿名子类实现多线程 -> run ... " + i);
}
}
}.start();
// 主线程执行代码...
for (int i = 0; i < 10; i++) {
System.out.println("main -> run ... " + i);
}
}
}
4. 线程的运行生命周期状态图
线程相关的知识点 :
1. 等待和唤醒机制.
2. 线程池 (优化线程的创建与销毁)
28. 网络编程 (了解)
网络可以完成多台计算机之间的数据交换.
网络三要素 :
1. IP 地址.
a) 作用 : 寻找网络上的唯一一台计算机.
2. port 端口号.
a) 作用 : 寻找计算机中的指定程序. (端口号可以由程序手动指定. 0~65535 [0~1024] 之间的端口号不要使用) http 80, https 443 ……
3. 协议.
a) 传输协议, 传送数据. (UDP 用户数据报协议 / TCP 输出控制协议)
IP 地址 :
此类表示互联网协议 (IP) 地址。
IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。
一个 IP 对象是由 `主机地址 + 主机名` 构成的.
package cn.panda.demo3;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class TestInetAddress {
public static void main(String[] args) throws UnknownHostException {
// 域名 -> 地址 DNS (Domain Name System)
InetAddress baidu = InetAddress.getByName("www.baidu.com");
System.out.println(baidu);
System.out.println(baidu.getHostName());
System.out.println(baidu.getHostAddress());
System.out.println("-------------");
InetAddress[] baidus = InetAddress.getAllByName("www.baidu.com");
for (InetAddress bd : baidus) {
System.out.println(bd);
}
System.out.println("-------------");
InetAddress localHost = InetAddress.getLocalHost();
// 10.254.4.143
// 192.168.xxx.xxx 内网 (B段)
// 第1种: 192.168.74.40 第2种: localhost 第3种: 127.0.0.1
System.out.println(localHost);
}
}
端口号 :
传输协议 :
1. UDP 协议 : 用户数据报协议, 类似于 `对讲机`.
特点 : 面向无连接协议.
好处 : 硬件资源消耗低. 通信效率高.
缺点 : 无法保证数据的安全性.
场景 : 视频会议, 共屏 …
2. TCP 协议 : 传输控制协议. 类似于 `电话机 / 手机`
特点 : 面向连接协议.
好处 : 可以保证数据的安全性与准确性.
缺点 : 硬件资源消耗高.
场景 : 下载, 上传, 重要数据 …
1. UDP实现互动聊天
此类表示用于发送和接收数据报数据包的套接字。 数据报套接字是分组传送服务的发送或接收点。
该类表示数据报包。数据报包用于实现无连接分组传送服务。
发送端实现步骤 :
1. 创建一个用于发送集装箱的码头 (无需指定端口号, 系统自动分配一个没有使用的端口号)
2. 准备一个用于发送数据的集装箱. (需要四个参数: 数据, 长度, IP对象, 端口号)
3. 使用码头发送集装箱.
4. 关闭码头.
package cn.panda.demo3;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPSender {
public static void main(String[] args) throws IOException {
// 1. 创建一个用于发送集装箱的码头 (无需指定端口号, 系统自动分配一个没有使用的端口号)
DatagramSocket datagramSocket = new DatagramSocket();
// 2. 准备一个用于发送数据的集装箱. (需要四个参数: 数据, 长度, IP对象, 端口号)
byte[] buf = "你好, 小姐姐, 约吗?".getBytes();
InetAddress address = InetAddress.getByName("192.168.74.40");
int port = 8888;
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, address, port);
// 3. 使用码头发送集装箱.
datagramSocket.send(datagramPacket);
// 4. 关闭码头.
datagramSocket.close();
}
}
接收端实现步骤 :
1. 创建一个用于接收的码头. (必须指定接收码头的端口号)
2. 创建一个用于接收数据的集装箱. (需要二个参数: 数据, 长度)
3. 使用码头接收数据到接收的集装箱中.
4. 解析一下数据.
5. 关闭码头.
package cn.panda.demo3;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPReciever {
public static void main(String[] args) throws IOException {
// 1. 创建一个用于接收的码头. (必须指定接收码头的端口号)
DatagramSocket datagramSocket = new DatagramSocket(8888);
// 2. 创建一个用于接收数据的集装箱. (需要二个参数: 数据, 长度)
byte[] buf = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
// 3. 使用码头接收数据到接收的集装箱中.
datagramSocket.receive(datagramPacket);
// 4. 解析一下数据.
byte[] data = datagramPacket.getData();
int len = datagramPacket.getLength();
InetAddress address = datagramPacket.getAddress();
int port = datagramPacket.getPort();
System.out.println(data);
System.out.println(len);
System.out.println(address);
System.out.println(port);
// 需求: 将接收到的数据拼接为字符串显示.
String str = new String(data, 0, len);
System.out.println(str);
// 5. 关闭码头.
datagramSocket.close();
}
}
思考 : 如何双向通信.
发送端实现步骤 :
1. 创建一个用于发送集装箱的码头 (无需指定端口号, 系统自动分配一个没有使用的端口号)
2. 准备一个用于发送数据的集装箱. (需要四个参数: 数据, 长度, IP对象, 端口号)
3. 使用码头发送集装箱.
4. 再准备一个用于接收数据的集装箱. (需求二个参数: 数据, 长度)
5. 使用码头接收数据到接收集装箱中.
6. 解析接收数据集装箱.
7. 关闭码头.
package cn.panda.demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPSender {
public static void main(String[] args) throws IOException {
// 发送端实现步骤 :
// 1. 创建一个用于发送集装箱的码头 (无需指定端口号, 系统自动分配一个没有使用的端口号)
DatagramSocket datagramSocket = new DatagramSocket();
// 2. 准备一个用于发送数据的集装箱. (需要四个参数: 数据, 长度, IP对象, 端口号)
byte[] buf = "小姐姐, 约吗?".getBytes();
InetAddress address = InetAddress.getByName("192.168.74.51");
int port = 8888;
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, address, port);
// 3. 使用码头发送集装箱.
datagramSocket.send(datagramPacket);
// 4. 再准备一个用于接收数据的集装箱. (需求二个参数: 数据, 长度)
byte[] receiveBuf = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuf, receiveBuf.length);
// 5. 使用码头接收数据到接收集装箱中.
datagramSocket.receive(receivePacket);
// 6. 解析接收数据集装箱.
byte[] data = receivePacket.getData();
int len = receivePacket.getLength();
InetAddress address2 = receivePacket.getAddress();
int port2 = receivePacket.getPort();
String str = new String(data, 0, len);
System.out.println(str);
System.out.println(address2 + " : " + port2);
// 7. 关闭码头.
datagramSocket.close();
}
}
接收端实现步骤 :
1. 创建一个用于接收的码头. (必须指定接收码头的端口号)
2. 创建一个用于接收数据的集装箱. (需要二个参数: 数据, 长度)
3. 使用码头接收数据到接收的集装箱中.
4. 解析一下数据.
5. 准备一个用于发送数据的集装箱. (需要四个参数: 数据, 长度, IP对象, 端口号)
6. 使用码头发送集装箱.
7. 关闭码头.
package cn.panda.demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPReceiver {
public static void main(String[] args) throws IOException {
// 接收端实现步骤 :
// 1. 创建一个用于接收的码头. (必须指定接收码头的端口号)
DatagramSocket datagramSocket = new DatagramSocket(8888);
// 2. 创建一个用于接收数据的集装箱. (需要二个参数: 数据, 长度)
byte[] buf = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
// 3. 使用码头接收数据到接收的集装箱中.
datagramSocket.receive(datagramPacket);
// 4. 解析一下数据.
byte[] data = datagramPacket.getData();
int length = datagramPacket.getLength();
String str = new String(data, 0, length);
System.out.println(str);
// 5. 准备一个用于发送数据的集装箱. (需要四个参数: 数据, 长度, IP对象, 端口号)
byte[] sendBuf = "叔叔, 不约.".getBytes();
InetAddress address = datagramPacket.getAddress();
int port = datagramPacket.getPort();
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, address, port);
// 6. 使用码头发送集装箱.
datagramSocket.send(sendPacket);
// 7. 关闭码头.
datagramSocket.close();
}
}
最后实现 : 互动聊天
特点: 随便发, 随便收, 互不影响.
发送端思路 :
package cn.panda.demo1;
public class SenderTask implements Runnable {
@Override
public void run() {
// 需求 : UDP 的发送端
// 1. 创建一个用于发送数据的码头
// 2. 创建一个用于发送数据的集装箱
// 3. 使用码头发送集装箱
// 4. 关闭码头
}
}
接收端思路 :
package cn.panda.demo1;
public class ReceiverTask implements Runnable {
@Override
public void run() {
// 需求 : UDP 的接收端
// 1. 创建一个用于接收数据的码头
// 2. 创建一个用于接收数据的集装箱
// 3. 使用码头将数据接收到集装箱中
// 4. 解析数据
// 5. 关闭码头
}
}
主类的代码 :
package cn.panda.demo1;
import java.io.IOException;
import java.net.DatagramSocket;
public class UDPChat {
public static void main(String[] args) throws IOException {
// 互动聊天 :
// 1. 创建一个码头
DatagramSocket datagramSocket = new DatagramSocket(10086);
// 2. `发送任务` 交给子线程独立执行
SenderTask senderTask = new SenderTask();
Thread senderThread = new Thread(senderTask, "发送线程");
senderThread.start();
// 3. `接收任务` 交给子线程独立执行
ReceiverTask receiverTask = new ReceiverTask();
Thread receiverThread = new Thread(receiverTask, "接收线程");
receiverThread.start();
// 后续代码 ...
}
}
主类实现了将 `发送和接收` 任务交给了子线程独立执行, 好处是, 发送与接收互不阻塞.
发送端任务代码的具体实现 :
package cn.panda.demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class SenderTask implements Runnable {
// 属性
private DatagramSocket datagramSocket;
private String ip;
private int port;
// 构造方法
public SenderTask(DatagramSocket datagramSocket, String ip, int port) {
this.datagramSocket = datagramSocket;
this.ip = ip;
this.port = port;
}
@Override
public void run() {
// 需求 : UDP 的发送端
// 1. 创建一个用于发送数据的码头 (不用)
// 2. 创建一个用于发送数据的集装箱
// 2.1 数据从键盘录入
Scanner sc = new Scanner(System.in);
String line = null;
while ((line = sc.nextLine()) != null) {
// 2.2 将读取的字符串转换为字节数据
byte[] buf = line.getBytes();
try {
InetAddress address = InetAddress.getByName(ip);
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, address, port);
// 3. 使用码头发送集装箱
datagramSocket.send(datagramPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
// 4. 关闭码头 (不要关闭, 因为不是发送类创建的)
sc.close();
}
}
接收端任务代码的具体实现 :
package cn.panda.demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class ReceiverTask implements Runnable {
// 属性
private DatagramSocket datagramSocket;
// 构造方法
public ReceiverTask(DatagramSocket datagramSocket) {
this.datagramSocket = datagramSocket;
}
@Override
public void run() {
// 需求 : UDP 的接收端
// 1. 创建一个用于接收数据的码头 (不用)
while (true) {
// 2. 创建一个用于接收数据的集装箱
byte[] buf = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
try {
// 3. 使用码头将数据接收到集装箱中
datagramSocket.receive(datagramPacket);
// 4. 解析数据
byte[] data = datagramPacket.getData();
int len = datagramPacket.getLength();
InetAddress address = datagramPacket.getAddress();
int port = datagramPacket.getPort();
String str = new String(data, 0, len);
System.out.println(address + " : " + port + " = " + str);
} catch (IOException e) {
e.printStackTrace();
}
}
// 5. 关闭码头
}
}
主业务逻辑的具体代码实现 :
package cn.panda.demo1;
import java.io.IOException;
import java.net.DatagramSocket;
public class UDPChat {
public static void main(String[] args) throws IOException {
// 互动聊天 :
// 1. 创建一个码头
DatagramSocket datagramSocket = new DatagramSocket(10086);
// 2. `发送任务` 交给子线程独立执行
String ip = "192.168.74.51";
int port = 10086;
SenderTask senderTask = new SenderTask(datagramSocket, ip, port);
Thread senderThread = new Thread(senderTask, "发送线程");
senderThread.start();
// 3. `接收任务` 交给子线程独立执行
ReceiverTask receiverTask = new ReceiverTask(datagramSocket);
Thread receiverThread = new Thread(receiverTask, "接收线程");
receiverThread.start();
// 后续代码 ...
}
}
2. TCP 实现多线程同时上传
HTTP : 超文本传送协议. 底层就是 TCP 协议.
客户端 : 浏览器
服务端 : Tomcat
package cn.panda.demo2;
public class TCPServer {
public static void main(String[] args) {
// 需求 : TCP 服务端
// 1. 创建一个服务端套接字 (注意: 必须绑定端口号)
// 2. 等待客户端连接 (阻塞方法)
// 3. 接收客户端数据 (获取socket通道输入流)
// 4. 回送给客户端数据 (获取 socket通道输出流)
// 注意: 往通道中输出数据后, 最后需要输出一个`结束符`
// 5. 关闭服务端套接字
}
}
package cn.panda.demo2;
public class TCPClient {
public static void main(String[] args) {
// 需求 : TCP 客户端
// 1. 创建一个客户端 (注意: 需要传递 `主机 + 端口`, 因为创建时客户端就在尝试三次握手)
// 2. 先发送数据给服务端 (获取 socket 通道的输出流)
// 3. 接收服务端的反馈 (获取 socket 通道的输入流)
// 4. 关闭客户端套接字
}
}
这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
package cn.panda.demo2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws IOException {
// 需求 : TCP 服务端
// 1. 创建一个服务端套接字 (注意: 必须绑定端口号)
ServerSocket serverSocket = new ServerSocket(8888);
// 2. 等待客户端连接 (阻塞方法)
Socket socket = serverSocket.accept();
// 3. 接收客户端数据 (获取socket通道输入流)
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = -1;
// read 方法就是一个阻塞方法.
/*
* 以前 : 读取的是文件信息. 文件到末尾, 自动就可以读取到 `结束符`.
* 现在 : 读取的是 socket 通道信息, 通道如果不输出结束符, 就无法读取到 -1.
*/
while ((len = in.read(buf)) != -1) {
String str = new String(buf, 0, len);
System.out.println(str);
}
// 4. 回送给客户端数据 (获取 socket通道输出流)
OutputStream out = socket.getOutputStream();
out.write("叔叔, 不约.".getBytes());
// 注意: 往通道中输出数据后, 最后需要输出一个`结束符`
socket.shutdownOutput();
// 5. 关闭服务端套接字
serverSocket.close();
}
}
该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通讯的端点。
package cn.panda.demo2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws IOException {
// 需求 : TCP 客户端
// 1. 创建一个客户端 (注意: 需要传递 `主机 + 端口`, 因为创建时客户端就在尝试三次握手)
Socket socket = new Socket("192.168.74.51", 8888);
// 2. 先发送数据给服务端 (获取 socket 通道的输出流)
OutputStream out = socket.getOutputStream();
out.write("小姐姐, 约吗?".getBytes());
// 注意: 往通道中输出数据后, 最后需要输出一个`结束符`
socket.shutdownOutput();
// 3. 接收服务端的反馈 (获取 socket 通道的输入流)
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
int len = -1;
// in.read(buf); 客户端等待端的回复.
while ((len = in.read(buf)) != -1) {
String str = new String(buf, 0, len);
System.out.println("服务端回送数据 : " + str);
}
// 4. 关闭客户端套接字
socket.close();
}
}
服务端上传服务端思路 :
package cn.panda.demo3;
/*
* TCP 服务端 :
*
* 数据源 : socket 通道.
* 目的地 : 服务器的硬盘路径.
*/
public class TCPServerPicture {
public static void main(String[] args) {
// 1. 创建一个服务端套接字 (指定接收的端口号)
// 2. 等待客户端连接
// 3. 读写操作 (字节流: BufferedInputStream & BufferedOutputStream)
// 4. 回送一条数据
// 5. 关闭资源
}
}
客户端上传思路 :
package cn.panda.demo3;
/*
* TCP 客户端 :
*
* 数据源 : 硬盘路径
* 目的地 : socket 通道
*/
public class TCPClientPicture {
public static void main(String[] args) {
// 1. 创建一个客户端的套接字 (注意: `主机 + 端口` 尝试连接,3次握手)
// 2. 读写操作 (字节流: BufferedInputSteram & BufferedOutputStream)
// 3. 接收服务端的反馈
// 4. 关闭客户端套接字
}
}
客户端上传代码实现 :
package cn.panda.demo3;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/*
* TCP 客户端 :
*
* 数据源 : 硬盘路径
* 目的地 : socket 通道
*/
public class TCPClientPicture {
public static void main(String[] args) throws IOException {
// 1. 创建一个客户端的套接字 (注意: `主机 + 端口` 尝试连接,3次握手)
Socket socket = new Socket("192.168.74.51", 10010);
// 2. 读写操作 (字节流: BufferedInputSteram & BufferedOutputStream)
// 2.1 创建两个高效的缓冲字节输入 / 输出流对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\Users\\Jack\\Pictures\\Saved Pictures\\8.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
// 2.2 读写数据
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
bos.flush();
}
// 思考 : 有没有往通道中写数据 ???
socket.shutdownOutput();
// 3. 接收服务端的反馈
InputStream in = socket.getInputStream();
byte[] readBuf = new byte[1024];
int readLen = -1;
while ((readLen = in.read(readBuf)) != -1) {
String str = new String(readBuf, 0, readLen);
System.out.println("服务端的反馈数据 : " + str);
}
// 4. 关闭客户端套接字
bis.close();
socket.close();
}
}
服务端上传代码实现 : 版本一
问题1 : 服务端只能接收一次客户端的连接.
问题2 : 如果同一个客户端传送两次, 第二次的数据就会将第一次的数据进行覆盖.
package cn.panda.demo3;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/*
* TCP 服务端 :
*
* 数据源 : socket 通道.
* 目的地 : 服务器的硬盘路径.
*/
public class TCPServerPicture {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务端套接字 (指定接收的端口号)
ServerSocket serverSocket = new ServerSocket(10010);
// 2. 等待客户端连接
Socket socket = serverSocket.accept();
// 3. 读写操作 (字节流: BufferedInputStream & BufferedOutputStream)
// 3.1 创建两个高效的缓冲字节输入 / 输出流
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
File parentFile = new File("C:\\Users\\Jack\\Desktop\\meinv");
// 判断父目录是否存在
if (parentFile.exists() == false) {
parentFile.mkdirs();
}
// 考虑图片名称 (ip)
String ip = socket.getInetAddress().getHostAddress();
File file = new File(parentFile, ip + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
// 3.2 读写数据
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
bos.flush();
}
// 4. 回送一条数据
OutputStream out = socket.getOutputStream();
out.write("上传成功!".getBytes());
// 考虑 : 有没有往通道中写数据 ???
socket.shutdownOutput(); // 结束符
// 5. 关闭资源
bos.close();
serverSocket.close();
}
}
问题1 : 图片名称的解决方案.
一个表示不可变的通用唯一标识符(UUID)的类。 UUID表示128位值。
package cn.panda.demo3;
import java.util.UUID;
public class TestUUID {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 替换
String str = UUID.randomUUID().toString().replaceAll("-", "");
System.out.println(str + ".jpg");
}
}
}
问题二 : 服务器不应该只能接收一次客户端的上传.
解决方案 : 多线程, 将上传任务交给子线程独立执行,这样就不会影响到主线程执行 serverSocket.accept(); 代码.
package cn.panda.demo3;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/*
* TCP 服务端 :
*
* 数据源 : socket 通道.
* 目的地 : 服务器的硬盘路径.
*/
public class TCPServerPicture {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务端套接字 (指定接收的端口号)
ServerSocket serverSocket = new ServerSocket(10010);
while (true) {
// 2. 等待客户端连接
Socket socket = serverSocket.accept();
// 3. 创建一个 `上传任务` 对象
UploadTask uploadTask = new UploadTask(socket);
Thread uploadThread = new Thread(uploadTask, "上传线程");
uploadThread.start();
}
// 服务器可以不关闭.
// serverSocket.close();
}
}
上传任务代码实现 :
package cn.panda.demo3;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.UUID;
public class UploadTask implements Runnable {
private Socket socket;
public UploadTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3. 读写操作 (字节流: BufferedInputStream & BufferedOutputStream)
// 3.1 创建两个高效的缓冲字节输入 / 输出流
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
File parentFile = new File("C:\\Users\\Jack\\Desktop\\meinv");
// 判断父目录是否存在
if (parentFile.exists() == false) {
parentFile.mkdirs();
}
// 考虑图片名称 (ip)
// String ip = socket.getInetAddress().getHostAddress();
String name = UUID.randomUUID().toString().replaceAll("-", "");
File file = new File(parentFile, name + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
// 3.2 读写数据
byte[] buf = new byte[1024];
int len = -1;
while ((len = bis.read(buf)) != -1) { // 5 分钟 (耗时操作)
bos.write(buf, 0, len);
bos.flush();
}
// 4. 回送一条数据
OutputStream out = socket.getOutputStream();
out.write("上传成功!".getBytes());
// 考虑 : 有没有往通道中写数据 ???
socket.shutdownOutput(); // 结束符
// 5. 关闭资源
bos.close();
} catch (IOException e) {
// 处理结果 ...
e.printStackTrace();
}
}
}
29. 反射 : (了解)
1. 反射图解说明 :
注意点 : 程序中不可以直接创建 Class 对象.
反射 :
作用 : 在程序运行期间动态创建对象. 写出通用性极高的程序.
使用场景 : 框架底层代码实现. (框架就是半成品)
请问 : 创建对象有几种方式 ???
1. new 类名(); 在程序编译阶段就确定了对象.
2. cls.newInstance(); 在程序运行阶段动态创建字符串表示的对象.
创建对象方式一 : 直接创建
package cn.panda.demo4;
public class Student {
}
Student stu = new Student();
System.out.println(stu);
创建对象方式二 : 反射创建
package cn.panda.demo4;
import java.io.FileReader;
import java.util.Properties;
public class TestReflection {
public static void main(String[] args) throws Exception {
// Properties 文件
Properties prop = new Properties();
prop.load(new FileReader("bean.properties"));
String className = prop.getProperty("bean");
Class<?> cls = Class.forName(className);
Object obj = cls.newInstance();
System.out.println(obj);
}
}
2. 类加载器 : (了解)
package cn.panda.demo2;
import sun.net.spi.nameservice.dns.DNSNameService;
public class TestClass {
public static void main(String[] args) {
// null 表示引导类加载器
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);
// sun 公司提供的扩展类 (程序的辅助类) 一般不让使用.
// 修改项目的访问规则 : 项目 -> properties -> build path -> library -> access ruls -> edit -> add -> sun/**
// ExtClassLoader -> Extension 扩展类加载器
ClassLoader classLoader2 = DNSNameService.class.getClassLoader();
System.out.println(classLoader2);
// AppClassLoader -> Application 应用类加载器
ClassLoader classLoader3 = TestClass.class.getClassLoader();
System.out.println(classLoader3);
}
}
类加载器的父子关系 :
package cn.panda.demo2;
public class TestClassLoader {
public static void main(String[] args) {
// AppClassLoader 应用类加载器
ClassLoader classLoader = TestClass.class.getClassLoader();
System.out.println(classLoader);
// ExtClassLoader 扩展类加载器
ClassLoader parent = classLoader.getParent();
System.out.println(parent);
// null 引导类加载器
ClassLoader parent2 = parent.getParent();
System.out.println(parent2);
}
}
3. Class对象的获取方式 :
Class 对象就是反射的基础. 使用反射操作第一件事情就是获取堆区的 Class 对象.
package cn.panda.demo2;
public class TestGetClass {
public static void main(String[] args) throws Exception {
// 1. Class.forName("包名+类名"); 全限定名称 (配置文件)
Class<?> cls = Class.forName("cn.panda.demo2.Student");
System.out.println(cls);
// 2. 类名.class 属性 常用场景 : 确定一个唯一的方法. `方法名+参数列表`
Class<?> cls2 = Student.class;
System.out.println(cls2);
System.out.println(cls == cls2);
// 调用方法, 传递参数
Object obj = new Student();
show(obj);
}
public static void show(Object obj) {
// 需求 : show 方法确定 obj 参数的具体类型
// 3. 对象名.getClass(); 方法
Class<?> cls3 = obj.getClass();
System.out.println(cls3);
}
}
4. Class对象创建描述的真实对象 :
实现一 : 创建一个默认属性值的对象. 直接使用 cls 对象, 调用 newInstance() 方法.
package cn.panda.demo2;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
/*
* JavaBean 类 :
* 1. 一个类必须是 public 公开的类.
* 2. 这个类必须要提供一个无参的公开的构造方法.
* 3. 属性私有化, 一定要提供对应的 getter & setter 方法.
*/
public class TestConstructor {
// 属性
private static Properties prop = null;
// 静态代码块
static {
prop = new Properties();
// 加载
try {
prop.load(new FileReader("bean.properties"));
} catch (IOException e) {
throw new RuntimeException("配置文件加载失败!");
}
}
public static void main(String[] args) throws Exception {
// Student stu = new Student();
// System.out.println(stu);
// 1. 取出 bean 对应的 `className` 类名称
String className = prop.getProperty("bean");
// 2. 根据 className 获取堆区中的 Class 对象
Class<?> cls = Class.forName(className);
// 3. 创建一个字符串表示的真实对象
Object obj = cls.newInstance();
System.out.println(obj);
}
}
实现二 : 创建一个带指定数据属性值的对象. 使用 cls 获取指定的 Constructor对象, 然后调用Constructor对象的newInstance(…) 方法.
package cn.panda.demo2;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Properties;
public class TestConstructor2 {
// 属性
private static Properties prop = null;
// 静态代码块
static {
prop = new Properties();
// 加载
try {
prop.load(new FileReader("bean.properties"));
} catch (IOException e) {
throw new RuntimeException("配置文件加载失败!");
}
}
public static void main(String[] args) throws Exception {
// Student stu = new Student("Jack", 30);
// System.out.println(stu);
// 1. 取出 bean 对应的 `className` 类名称
String className = prop.getProperty("bean");
// 2. 根据 className 获取堆区中的 Class 对象
Class<?> cls = Class.forName(className);
// 3. 获取对应的 Constructor 构造方法对象
// Class<?>... parameterTypes 参数类型为 Class 类型的可变参数. 可变参数底层就是数组.
Constructor<?> constructor = cls.getConstructor(new Class[]{String.class, int.class});
// 4. 使用构造方法, 创建真实对象
// Object... 可变参数底层就是数组. 说白了, 就是需要一个 Object[]
Object obj = constructor.newInstance(new Object[]{"Jack", 30});
System.out.println(obj);
}
}
实现三 : 调用私有构造方法, 创建对象.
package cn.panda.demo2;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Properties;
public class TestConstructor3 {
// 属性
private static Properties prop = null;
// 静态代码块
static {
prop = new Properties();
// 加载
try {
prop.load(new FileReader("bean.properties"));
} catch (IOException e) {
throw new RuntimeException("配置文件加载失败!");
}
}
public static void main(String[] args) throws Exception {
// Student stu = new Student("Jack", 30, "学霸兼学渣");
// System.out.println(stu);
// 1. 取出 bean 对应的 `className` 类名称
String className = prop.getProperty("bean");
// 2. 根据 className 获取堆区中的 Class 对象
Class<?> cls = Class.forName(className);
// 3. 获取对应的 Constructor 构造方法对象
// Class<?>... parameterTypes 参数类型为 Class 类型的可变参数. 可变参数底层就是数组.
Constructor<?> constructor = cls.getDeclaredConstructor(new Class[]{String.class, int.class, String.class});
// 4. 暴力访问
constructor.setAccessible(true);
// 5. 使用构造方法, 创建真实对象
// Object... 可变参数底层就是数组. 说白了, 就是需要一个 Object[]
Object obj = constructor.newInstance(new Object[]{"Jack", 30, "学霸兼学渣"});
System.out.println(obj);
}
}
5. Class 对象设置属性 :
package cn.panda.demo3;
public class TestField {
public static void main(String[] args) {
Student stu = new Student();
stu.description = "学霸兼学渣";
System.out.println(stu);
}
}
需求一 : 反射设置公开的属性数值.
package cn.panda.demo3;
import java.lang.reflect.Field;
public class TestField {
public static void main(String[] args) throws Exception {
// 1. 创建一个 Class 对象
Class<?> cls = Class.forName("cn.panda.demo3.Student");
Object obj = cls.newInstance();
// 2. 获取属性对象
Field field = cls.getField("description");
// 3. 设置属性数值
field.set(obj, "学霸兼学渣");
System.out.println(obj);
}
}
需求二 : 反射设置私有的属性数值.
package cn.panda.demo3;
import java.lang.reflect.Field;
public class TestField2 {
public static void main(String[] args) throws Exception {
// 1. 创建一个 Class 对象
Class<?> cls = Class.forName("cn.panda.demo3.Student");
Object obj = cls.newInstance();
// 2. 获取属性对象
Field field = cls.getDeclaredField("name");
// 3. 设置暴力访问
field.setAccessible(true);
// 4. 设置属性数值
field.set(obj, "渣渣辉");
System.out.println(obj);
}
}
需求三 : 获取属性数值.
// 说明 : field 对象就是 name 属性.
// 说明 : 从 obj 对象中获取 name 属性的值.
Object name = field.get(obj);
System.out.println(name);
6. Class对象调用方法 :
需求一 : 反射调用并执行公开的方法.
package cn.panda.demo3;
import java.lang.reflect.Method;
public class TestMethod {
public static void main(String[] args) throws Exception {
// 1. 获取字符串表示的 Class 对象
Class<?> cls = Class.forName("cn.panda.demo3.Student");
Object obj = cls.newInstance();
// 2. 使用 cls 对象获取方法对象
Method method = cls.getMethod("setName", new Class[]{String.class});
// 3. 执行方法
method.invoke(obj, new Object[]{"渣渣辉"});
System.out.println(obj);
}
}
需求二 : 反射调用并执行私有的方法.
package cn.panda.demo3;
import java.lang.reflect.Method;
public class TestMethod2 {
public static void main(String[] args) throws Exception {
// 1. 获取字符串表示的 Class 对象
Class<?> cls = Class.forName("cn.panda.demo3.Student");
Object obj = cls.newInstance();
// 2. 使用 cls 对象获取方法对象
Method method = cls.getDeclaredMethod("show", new Class[]{int.class});
// 3. 设置暴力访问
method.setAccessible(true);
// 4. 执行方法
method.invoke(obj, new Object[]{998});
}
}
7. 反射综合案例 (举办晚会)
说明 : 主业务逻辑就是举办晚会. 也就是说: 唱歌, 跳舞, 表演 仅仅就是主业务逻辑执行的一部分.
实现一 : 面向打印的思想.
package cn.panda.demo5;
public class TestEveningParty {
public static void main(String[] args) {
System.out.println("晚会开始了...");
// 张学友唱歌
System.out.println("张学友 演唱 `一路上有你`.");
// 少女时代跳舞
System.out.println("少女时代 跳 `Gee Gee Gee.");
// 赵本山表演
System.out.println("赵本山 表演 `卖拐`.");
System.out.println("难忘今宵!");
}
}
实现二 : 面向对象编程.
package cn.panda.demo5;
public class TestEveningParty {
public static void main(String[] args) {
System.out.println("晚会开始了...");
// 张学友唱歌
ZhangXueYou zhangXueYou = new ZhangXueYou();
zhangXueYou.sing();
// 少女时代跳舞
GirlsGeneration girls = new GirlsGeneration();
girls.dance();
// 赵本山表演
ZhaoBenShan zhaoBenShan = new ZhaoBenShan();
zhaoBenShan.perform();
System.out.println("难忘今宵!");
}
}
需求 : 更换所有 `演出的对象`.
package cn.panda.demo5;
public class TestEveningParty {
public static void main(String[] args) {
System.out.println("晚会开始了...");
// 刘德华唱歌
LiuDeHua liuDeHua = new LiuDeHua();
liuDeHua.sing();
// 少女团队跳舞
GirlsTeam girls = new GirlsTeam();
girls.dance();
// 刘谦表演
LiuQian liuQian = new LiuQian();
liuQian.perform();
System.out.println("难忘今宵!");
}
}
问题 : 程序的维护性非常差. 更换对象时, 与对象相关的所有代码与对象紧密相连了. 因此统统都需要修改.
效果 : 更换对象, 不更换其它代码. 保证对象与调用的方法解耦.
现象 :
1. 唱歌 (刘德华, 张学友 …) Singable
2. 跳舞 (少女时代, 少女团伙 …) Dancable
3. 表演 (赵本山, 刘谦 …) Performable
解决方案 : 将所有的行为, 都定义到接口中. 让行为与类进行解耦.
package cn.panda.demo5;
public interface Singable {
void sing();
}
package cn.panda.demo5;
public interface Dancable {
void dance();
}
package cn.panda.demo5;
public interface Performable {
void perform();
}
接口的具体实现类 :
package cn.panda.demo5;
public class ZhangXueYou implements Singable {
@Override
public void sing() {
System.out.println("张学友 演唱 `一路上有你`.");
}
}
package cn.panda.demo5;
public class LiuDeHua implements Singable {
@Override
public void sing() {
System.out.println("刘德华 演唱 `爱你一万年`.");
}
}
package cn.panda.demo5;
public class GirlsGeneration implements Dancable {
@Override
public void dance() {
System.out.println("少女时代 跳 `Gee Gee Gee.");
}
}
package cn.panda.demo5;
public class GirlsTeam implements Dancable {
@Override
public void dance() {
System.out.println("少女团伙 跳 `性感广场舞`.");
}
}
package cn.panda.demo5;
public class ZhaoBenShan implements Performable {
@Override
public void perform() {
System.out.println("赵本山 表演 `卖拐`.");
}
}
package cn.panda.demo5;
public class LiuQian implements Performable {
@Override
public void perform() {
System.out.println("刘谦 表演 `大变死人`.");
}
}
晚会逻辑实现一 :
package cn.panda.demo5;
public class TestEveningParty {
public static void main(String[] args) {
System.out.println("晚会开始了...");
// 1. 来一个会唱歌的
Singable s = new LiuDeHua();
s.sing();
// 2. 来一个会跳舞的
Dancable d = new GirlsTeam();
d.dance();
// 3. 来一个会表演的
Performable p = new LiuQian();
p.perform();
System.out.println("难忘今宵!");
}
}
晚会逻辑实现二 :
package cn.panda.demo5;
public class TestEveningParty {
public static void main(String[] args) {
System.out.println("晚会开始了...");
// 1. 来一个会唱歌的
Singable s = new ZhangXueYou();
s.sing();
// 2. 来一个会跳舞的
Dancable d = new GirlsGeneration();
d.dance();
// 3. 来一个会表演的
Performable p = new ZhaoBenShan();
p.perform();
System.out.println("难忘今宵!");
}
}
问题 : 既然主业务逻辑是举办晚会, 那到底来哪些演员与主业务逻辑有关系吗 ??? 没有关系.
晚会开始了…
// 1. 来一个会唱歌的
// 2. 来一个会跳舞的
// 3. 来一个会表演的
难忘今宵…
既然没有关系, 那么应该由 `谁` 来提供晚会举办的对象呢 ??? 应该由一个 `晚会工厂类` 专门来提供举办晚会的对象. PartyFactory (工厂设计模式)
package cn.panda.demo5;
public class TestEveningParty {
public static void main(String[] args) {
System.out.println("晚会开始了...");
// 1. 来一个会唱歌的
Singable s = PartyFactory.getSingable();
s.sing();
// 2. 来一个会跳舞的
Dancable d = PartyFactory.getDancable();
d.dance();
// 3. 来一个会表演的
Performable p = PartyFactory.getPerformable();
p.perform();
System.out.println("难忘今宵!");
}
}
最后的重点 : PartyFactory 晚会工厂类如何实现 ???
package cn.panda.demo5;
public class PartyFactory {
// 1. 创建一个会唱歌的对象
public static Singable getSingable() {
return null;
}
// 2. 创建一个会跳舞的对象
public static Dancable getDancable() {
return null;
}
// 3. 创建一个会表演的对象
public static Performable getPerformable() {
return null;
}
}
工厂设计模式的具体代码实现 :
import java.util.Properties;
// 工厂设计模式 : 配置文件 + 反射
// 问题 : 配置文件中的信息, 应该在哪个位置实现读取 ???
// 说明1 : 配置文件中的信息, 不可以在构造方法中读取, 因为静态方法不需要对象, 因此, 如何直接使用类名调用静态方法, 此时信息还不存在.
// 说明2 : 1. 配置文件中的信息, 一定要尽早加载. 2. 配置文件中的信息无需加载多次, 在程序启动时, 仅需要被加载一次.
public class PartyFactory {
// 属性
private static Properties prop = null; // 1. 定义
// 静态代码块 : 加载配置文件
/*
* 特点1 : 静态代码块在类加载的最后一步执行.
* 特点2 : 一个类仅会被 Java 虚拟机加载一次. 那么, 静态代码块在整个程序运行过程中, 仅会被执行一次.
*/
static {
// System.out.println("PartyFactory 类的静态代码块被执行 ...");
// 1. 创建一个 Properties 对象
prop = new Properties(); // 2. 初始化
try {
// 2. 将配置文件信息加载到 prop 对象中
prop.load(new FileReader("party.properties"));
} catch (IOException e) {
// 需求 : 如果配置文件加载失败, 抛出一个运行时, 终止程序.
throw new RuntimeException("配置文件加载失败!");
// e.printStackTrace();
}
}
// 1. 创建一个会唱歌的对象
public static Singable getSingable() throws Exception {
// cn.panda.demo1.LiuDeHua -> 类名称
// 1. 根据当前方法需要的 key, 获取配置文件中的对应类名
String className = prop.getProperty("Singable");
// 2. 根据 Class 类的 forName() 方法, 获取字符串表示的 `真实对象`
Class<?> cls = Class.forName(className);
// 3. 根据当前的 cls 对象, 调用 newInstance() 方法, 创建出字符串表示的真实对象
Singable s = (Singable) cls.newInstance();
// 4. 返回创建的对象
return s;
}
// 2. 创建一个会跳舞的对象
public static Dancable getDancable() throws Exception {
// cn.panda.demo1.GrilsTeam -> 类名称
// 1. 根据当前方法需要的 key, 获取配置文件中的对应类名
String className = prop.getProperty("Dancable");
// 2. 根据 Class 类的 forName() 方法, 获取字符串表示的 `真实对象`
Class<?> cls = Class.forName(className);
// 3. 根据当前的 cls 对象, 调用 newInstance() 方法, 创建出字符串表示的真实对象
Dancable d = (Dancable) cls.newInstance();
// 4. 返回创建的对象
return d;
}
// 3. 创建一个会表演的对象
public static Performable getPerformable() throws Exception {
// cn.panda.demo1.LiuQian -> 类名称
// 1. 根据当前方法需要的 key, 获取配置文件中的对应类名
String className = prop.getProperty("Performable");
// 2. 根据 Class 类的 forName() 方法, 获取字符串表示的 `真实对象`
Class<?> cls = Class.forName(className);
// 3. 根据当前的 cls 对象, 调用 newInstance() 方法, 创建出字符串表示的真实对象
Performable p = (Performable) cls.newInstance();
// 4. 返回创建的对象
return p;
}
}
配置文件信息 :
#Singable=cn.panda.demo1.ZhangXueYou
#Dancable=cn.panda.demo1.GirlsTeam
#Performable=cn.panda.demo1.LiuQian
Singable=cn.panda.demo1.LiuDeHua
Dancable=cn.panda.demo1.GirlsGeneration
Performable=cn.panda.demo1.ZhaoBenShan
8. Computer 运行案例升级 :
配置文件信息 :
package cn.panda.demo4;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class TestComputer {
// 属性
private static Properties prop;
// 静态代码块
static {
prop = new Properties();
// 加载
try {
prop.load(new FileReader("usb.properties"));
} catch (IOException e) {
// 将编译时期的异常转换为运行时期的异常抛出 ...
throw new RuntimeException("配置文件加载失败!");
}
}
public static void main(String[] args) throws Exception {
Computer c = new Computer();
c.run();
// 使用外接设备
// prop 对象有没有长度呢 ???
for (int i = 1; i <= prop.size(); i++) {
// 1. 获取 prop 对象中获取 `字符串表示的类名称`
String className = prop.getProperty("USB" + i);
// 2. 使用 className 字符串表示的 Class 对象
Class<?> cls = Class.forName(className);
// 3. 创建真实对象
USB usb = (USB) cls.newInstance();
// 4. 使用外接设备, 将反射创建的对象传入
c.useUSB(usb);
}
}
}
30. 注解 (配置文件)
1. 使用 Junit 框架的注解 :
package cn.panda.demo5;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/*
* 框架 / 组件 / 工具类 一般使用的步骤 :
* 1. 需要去官网下载对应的 jar 包.
* 2. 在当前项目中导入 jar 包. 并将该 jar 编译到 build path.
* 3. 之后就可以在项目中使用.
*
* Junit Java 单元测试框架不需要去官网下载 jar 包. Eclipse 集成了 Junit 框架.
*
* 作用 : 取代 main 方法运行.
*/
public class TestJUnit {
@Before
public void before() {
System.out.println("before 方法被执行 ...");
}
@After
public void after() {
System.out.println("after 方法被执行 ...");
}
@Test
public void test1() {
System.out.println("测试方法一被执行 ...");
}
@Test
public void test2() {
System.out.println("测试方法二被执行 ...");
}
@Test
public void test3() {
System.out.println("测试方法三被执行 ...");
}
}
2. JDK 提供的三个注解 :
注解 : 给编译器查看的.
注释 : 给程序员看的.
对比注释:注释是给开发人员阅读的,注解是给计算机提供相应信息的。
l 什么是注解:Annotation注解,是一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次
l 注解的作用:
- 编译检查:通过代码里标识注解,让编译器能够实现基本的编译检查。例如:@Override
- 代码分析:通过代码里标识注解,对代码进行分析。 例如 : @Test
- 编写文档:通过代码里标识注解,辅助生成帮助文档对应的内容 @Deprecated
目的 : 为了替代配置文件. xml -> 替代配置文件 properities 属性集类
JDK 5.0 之后出现注解的技术 :
JDK官方提供了三个注解
@Override: 限定重写父类方法, 该注解只能用于方法 ------ 编译时检查,不构成覆盖 报错
* JDK5.0 override注解只能用于方法覆盖 JDK6.0 该注解可以用于接口的方法实现
@Deprecated: 用于表示某个程序元素(类, 方法等)已过时 ----- 在编译器进行编译时 报出一个警告
* 为什么会有过时方法: 提供了新的方法取代之前方法、发现某个方法存在安全隐患,不建议用户再使用
@SuppressWarnings: 抑制编译器警告. ---- 通知编译器在编译时 不要报出警告信息
* 使用 all 忽略所有警告信息
package cn.panda.demo5;
import java.util.ArrayList;
@SuppressWarnings("all")
public class TestJDKAnnotation {
public static void main(String[] args) {
/*
* @Override 重写父类方法 / 实现接口中定义方法时会使用, 让编译器检查被该注解修饰的方法是否为重写的方法.
*
* @Deprecated 过时的. 标识过时的方法, 不建议使用, 但是依然可以使用.
*
* @SuppressWarnings(信息); 抑制警告
*/
Dog dog = new Dog();
dog.shout();
Wolf wolf = new Wolf();
wolf.shout();
// wolf.lovingSheep();
wolf.lovingToEatSheep();
int num1 = 10;
// @SuppressWarnings("unused")
int num2 = 20;
int sum = num1 + num1;
System.out.println(sum);
// @SuppressWarnings({"unused", "rawtypes"})
ArrayList list = new ArrayList();
}
}
// 定义一个父类
class Animal {
// 行为 :
public void shout() {
System.out.println("随便叫 ...");
}
}
// 1.0 版本提供一个 lovingSheep() 方法.
// 2.0 升级了该方法, 替换了该方法. 因为这个方法可能有安全隐患, 不希望被使用 ... lovingToEatSheep();
class Wolf extends Animal {
// 重写父类的 shout 方法
@Override
public void shout() {
System.out.println("嗷嗷嗷 ...");
}
@Deprecated
public void lovingSheep() {
System.out.println("狼爱上了羊 ...");
}
public void lovingToEatSheep() {
System.out.println("狼爱上了吃羊 ...");
}
}
// 定义一个子类
class Dog extends Animal {
// 重写 shout() 方法
@Override
public void shout() {
System.out.println("汪汪汪 ...");
}
}
2. 自定义注解类, 并完成解析 (了解)
自定义注解 :
package cn.panda.demo1;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 自定义一个 `注解` 类
// 作用 : 提供配置信息.
// 元注解 : 修饰注解的注解.
// 1. 注解的使用范围 (类, 方法, 属性 ...)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
// 2. 注解的信息需要保留到什么阶段, 如果保留到运行阶段, 因此就可以使用反射读取注解信息, 具体查看(AnnotatedElement 接口)
@Retention(RetentionPolicy.RUNTIME)
// (了解)
@Inherited // 继承
@Documented // 文档
public @interface Description {
// 注解只能提供属性, 没有方法.
String desc();
String author();
int age() default 18;
}
使用注解 : (重点)
package cn.panda.demo1;
public class Student {
// 属性
// 行为
@Description(desc="学霸兼学渣", author="Jack", age=30)
public void study() {
System.out.println("好好学习, 天天向上.");
}
}
解析注解 : (了解)
package cn.panda.demo1;
import java.lang.reflect.Method;
public class TestAnnotation {
public static void main(String[] args) throws Exception {
// Class 对象
Class<?> cls = Class.forName("cn.panda.demo1.Student");
// Method 对象
Method method = cls.getMethod("study", new Class[]{});
// 调用 AnnotatedElement 接口中定义的行为
// 判断
if (method.isAnnotationPresent(Description.class)) {
// 成立, 获取注解对象
Description annotation = method.getAnnotation(Description.class);
// 获取注解对象中的信息
String desc = annotation.desc();
String author = annotation.author();
int age = annotation.age();
System.out.println(desc + " : " + author + " : " + age);
}
System.out.println("执行完成...");
}
}
注解练习 : (定义, 使用与解析)
定义注解 :
package cn.panda.demo2;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 1. 使用范围
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
// 2. 保留策略
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
// 属性
String value();
}
使用注解 :
package cn.panda.demo2;
// @MyAnnotation(value="渣渣辉")
@MyAnnotation("渣渣辉")
public class Student {
// 属性
@MyAnnotation("panda_007")
private String id;
// 行为
@MyAnnotation("Java")
public void study() {
System.out.println("好好学习, 天天向上.");
}
}
解析注解 :
package cn.panda.demo2;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class TestMyAnnotation {
public static void main(String[] args) throws Exception {
// 解析 :
// 类
Class<?> cls = Class.forName("cn.panda.demo2.Student");
// 判断
if (cls.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = cls.getAnnotation(MyAnnotation.class);
String value = annotation.value();
System.out.println(value);
}
// 属性
Field field = cls.getDeclaredField("id");
if (field.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
String value = annotation.value();
System.out.println(value);
}
// 方法
Method method = cls.getMethod("study", new Class[]{});
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
String value = annotation.value();
System.out.println(value);
}
}
}
3. 自定义@Test实现 (了解)
package cn.panda.demo3;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 1. 使用位置
@Target(ElementType.METHOD)
// 2. 保留策略
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
// 不需要提供信息
}
package cn.panda.demo3;
public class TestMyTest {
@MyTest
public void test1() {
System.out.println("测试一 ...");
}
@MyTest
public void test2() {
System.out.println("测试二 ...");
}
@MyTest
public void test3() {
System.out.println("测试三 ...");
}
}
package cn.panda.demo3;
import java.lang.reflect.Method;
public class MyTestClass {
public static void main(String[] args) throws Exception {
// Class 对象
Class<?> cls = Class.forName("cn.panda.demo3.TestMyTest");
Object obj = cls.newInstance();
// 获取该类的所有方法
Method[] methods = cls.getMethods();
// 遍历
for (Method method : methods) {
// 判断
if (method.isAnnotationPresent(MyTest.class)) {
// 条件成立, 说明当前方法需要被运行 ...
// method.invoke(obj, new Object[]{});
method.invoke(obj);
}
}
}
}
31. 动态代理 (了解)
说明 : JDK 提供的一个非常重要的技术, 这个技术可以实现方法的 `拦截与控制`. 改写方法
@Test
public void test1() {
// 1. 创建一个 `SuperStar接口` 的实现类
SuperStar star = new LiuYan();
// 经纪人 / 中介 / 代理商 ...
// 2. 执行 `SuperStar接口` 中定义的行为规范
star.sing(10);
star.liveShow(20);
star.sleep();
}
1. Proxy 参数讲解 :
2. SuperStar 歌手接口
定义了一个接口 :
package cn.panda.demo4;
public interface SuperStar {
void sing(int money);
void liveShow(int money);
void sleep();
}
接口的实现类定义 :
package cn.panda.demo4;
public class LiuYan implements SuperStar {
@Override
public void sing(int money) {
System.out.println("柳岩 演唱了一首 `真的真的很爱你`. 挣了 : " + money);
}
@Override
public void liveShow(int money) {
System.out.println("柳岩 参加了 <<Running man>> 节目. 挣了 : " + money);
}
@Override
public void sleep() {
System.out.println("柳岩 `真的真的` 很累了, 休息一下...");
}
}
调用处理程序 :
package cn.panda.demo4;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
// 属性
private SuperStar star;
// 构造方法
public MyInvocationHandler(SuperStar star) {
this.star = star;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// System.out.println("method = " + method.getName());
// System.out.println("args = " + Arrays.toString(args));
System.out.println("调用之前需要执行的代码 ...");
Object result = method.invoke(star, args);
System.out.println("调用之后需要处理的代码 ...");
return result;
}
}
测试类 :
// 框架中, 我们会使用框架的很多方法, 很多方法返回的都是 `代理对象`, 而不是真实对象 ...
// JDBC Java连接数据库 ... c3p0, DBCP 数据库连接池框架 ...
// 提供了一个方法 : getConnection 获取连接对象.
// 说明 : 调用该方法, 获取的不是连接对象, 而是获取了一个 `连接的代理对象`.
@Test
public void test2() {
// 1. 创建一个 `SuperStar接口` 的实现类
SuperStar star = new LiuYan();
// 经纪人 / 中介 / 代理商 ... (Proxy 代理类)
ClassLoader loader = star.getClass().getClassLoader();
Class<?>[] interfaces = star.getClass().getInterfaces();
// MyInvocationHandler 该类必须实现 InvocationHandler 接口
SuperStar proxy = (SuperStar) Proxy.newProxyInstance(loader, interfaces, new MyInvocationHandler(star));
// 2. 执行 `代理对象` 中定义的行为规范
proxy.sing(10);
proxy.liveShow(20);
proxy.sleep();
}
说明 : 调用代理对象的任何方法, 最终都会将方法的执行交给 `调用处理器` 对象的 invoke 方法实现处理.
@Test
public void test3() {
// 1. 创建一个 `SuperStar接口` 的实现类
SuperStar star = new LiuYan();
/********** 动态代理 start ***********/
ClassLoader loader = star.getClass().getClassLoader();
Class<?>[] interfaces = star.getClass().getInterfaces();
// new InvocationHandler(){} 匿名调用处理器对象
SuperStar proxy = (SuperStar) Proxy.newProxyInstance(loader, interfaces, new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 判断 对代理对象的方法进行判断
if ("sing".equals(method.getName())) {
// 取出金额
int money = (int) args[0];
// 判断金额
if (money < 10000) {
System.out.println("滚, 你这个穷屌丝, 回家撸代码去吧...");
return null;
}
// 金额足够
// 抽提成 ...
System.out.println("代理抽取了 : " + (money * 0.3) + " 元.");
// 执行被代理对象的真实方法
return method.invoke(star, (int) (money * 0.7));
} else if ("liveShow".equals(method.getName())) {
// 取出金额
int money = (int) args[0];
// 判断金额
if (money < 100000) {
System.out.println("滚, 怎么还是你这个穷屌丝, 继续回家撸代码去吧...");
return null;
}
// 金额足够
// 抽提成 ...
System.out.println("代理抽取了 : " + (money * 0.3) + " 元.");
// 执行被代理对象的真实方法
return method.invoke(star, (int) (money * 0.7));
}
// 其余的方法, 直接执行, 不拦截 ...
return method.invoke(star, args);
}
});
/********** 动态代理 end ***********/
// 2. 执行 `代理对象` 中定义的行为规范
proxy.sing(10000);
proxy.liveShow(200000);
proxy.sleep();
}
关于调用处理器类中 invoke 方法形参的 proxy 说明 :
// Proxy 对象很少被使用. 调用 proxy 对象任何方法, 都会交给 `调用处理器` 对象的 invoke 方法.
System.out.println(proxy.getClass()); // class com.sun.proxy.$Proxy4 内部类对象
//补充,关于返回值的问题 object
public class DemoTest {
@Test
public void test() throws Exception {
Object returnObj = returnObj(); //returnObj()方法被调用,getSum()就得到了执行
System.out.println(returnObj); //50
}
public Object returnObj() throws Exception{
return getSum(10,40); //getSum()是有返回值作为结果的,是一个值,这个值被作为returnObj()方法的返回值返回
}
public int getSum(int a,int b){ //该方法有返回值
System.out.println("huahuashijie");
return a+b;
}
}
public class DemoTest {
@Test
public void test() throws Exception {
Object returnObj = returnObj(); //returnObj();此方法被调用,getSum()就得到了执行
System.out.println(returnObj);
}
public Object returnObj() throws Exception{
return getSum(10,40); 报错了 这里getSum()结果为void
}
public void getSum(int a,int b){ //该方法没有返回值
System.out.println("huahuashijie");
System.out.println(a+b);
}
}
改成反射:
public class DemoTest {
@Test
public void test() throws Exception {
Object returnObj = returnObj(); //returnObj();此方法被调用,getSum()就得到了执行
System.out.println(returnObj); //null
}
public Object returnObj() throws Exception{
Class<?> cls = Class.forName("cn.panda.test.DemoTest");
Object obj = cls.newInstance();
//反射获取方法对象
Method method = cls.getMethod("getSum", int.class,int.class);
//执行此方法,返回值或许有或许没有,如果没有,返回值就是null
//如果没有此行代码,此方法就得不到执行
Object invoke = method.invoke(obj, 10,50); //这行代码至关重要,返回值是Object,说明一定有返回值,即使为null,但也不是void了
//上面调用方法有返回值,Object了, 或许是null,但不是void(没有返回值)
return invoke;
}
public void getSum(int a,int b){ //该方法没有返回值
System.out.println("huahuashijie");
System.out.println(a+b);
}
}
有个疑问:
为啥非静态方法可以直接被调用了,没通过对象调用. 测试环境可以,测试方法是非静态的
32. XML 可扩展标记语言 (配置文件)
1. 如何去写一个XML
store.xml
约束 : DTD, Schema
<?xml version="1.0" encoding="UTF-8"?>
<!-- 注释: 一个xml文件只能拥有一个 `根标签` -->
<store>
<!-- 信息有两种存储方式. 1. 标签头中的属性存储 2. 在标签体中存储信息-->
<!-- 格式 : key = value -->
<product category="手机数码">
<pid>100</pid>
<pname>华为M10</pname>
<price>1999</price>
<!-- 如果标题体中没有信息数据, 可以定义为自关闭标签 -->
<bean name="username" value="123456"></bean>
<bean name="username" value="123456" />
</product>
<product category="电脑办公">
<pid>200</pid>
<pname>三星笔记本</pname>
<price>2999</price>
</product>
<product category="大型家电">
<pid>300</pid>
<pname>海尔洗衣机</pname>
<price>999</price>
</product>
</store>
books.xml
<?xml version="1.0" encoding="UTF-8"?>
<books>
<book author="张三丰">
<name>Java从入门到放弃</name>
<price>998</price>
</book>
<book author="灭绝师太">
<name>Java编程思想</name>
<price>98</price>
</book>
</books>
2. 解析 xml 文件
xml 文件 :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="001" className="cn.panda.User">
<property name="userName" value="Jack"></property>
<property name="password" value="123456"></property>
</bean>
<bean id="002" className="cn.panda.User">
<property name="userName" value="Peter"></property>
<property name="password" value="654321"></property>
</bean>
</beans>
解析实现 :
package cn.panda.demo5;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.junit.Test;
public class TestParseXML {
@Test
public void test1() throws Exception {
// 1. 创建一个 SAXReader 对象
SAXReader saxReader = new SAXReader();
// 2. 调用 saxReader 对象 read 方法, 将 xml 数据读取到内存, 形成一颗文档树
Document document = saxReader.read("beans.xml");
// 3. 使用 document 获取 xml 的根标签对象
Element root = document.getRootElement();
// 4. 获取 `子标签 / 子元素`
List<Element> beanElements = root.elements();
// 5. 遍历子元素集合
for (Element bean : beanElements) {
// 6. 获取 bean 对象中的属性数据
String id = bean.attributeValue("id");
String className = bean.attributeValue("className");
System.out.println(id + " : " + className);
// 7. 获取 bean 元素中的子元素
List<Element> propElements = bean.elements();
// 8. 遍历 propElements 集合
for (Element prop : propElements) {
// 9. 获取 prop 对象的属性值
String name = prop.attributeValue("name");
String value = prop.attributeValue("value");
System.out.println("\t" + name + " = " + value);
}
}
}
}
xml 文件 :
<?xml version="1.0" encoding="UTF-8"?>
<servlets>
<servlet>
<servlet-name>MyServlet1</servlet-name>
<servlet-class>cn.panda.MyServlet1</servlet-class>
</servlet>
<servlet>
<servlet-name>MyServlet2</servlet-name>
<servlet-class>cn.panda.MyServlet2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet1</servlet-name>
<url-pattern>/s1</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>MyServlet2</servlet-name>
<url-pattern>/s2</url-pattern>
</servlet-mapping>
</servlets>
解析实现 :
@Test
public void demo1() throws Exception {
// 1. 创建一个 SaxReader 对象
SAXReader saxReader = new SAXReader();
// 2. 使用 SaxReader 对象调用 read 方法, 将xml数据读取到内存, 然后形成一颗文档树对象
Document document = saxReader.read("servlets.xml");
// 3. 使用 `文档树对象` 获取根标签对象
Element root = document.getRootElement();
// 4. 使用根标签获取指定的子标签数据 elements("子标签名称");
List<Element> servletsElements = root.elements("servlet");
// 5. 遍历标签集合
for (Element servlet : servletsElements) {
// 6. 直接通过父标签获取子标签体中数据
String name = servlet.elementText("servlet-name");
String cls = servlet.elementText("servlet-class");
System.out.println(name + " = " + cls);
}
// elements("servlet-mapping");
List<Element> mappingElements = root.elements("servlet-mapping");
for (Element mapping : mappingElements) {
String name = mapping.elementText("servlet-name");
String url = mapping.elementText("url-pattern");
System.out.println(name + " = " + url);
}
}
33. BeanUtils 工具类 (了解)
JavaBean 类 :
1. 该类必须要公开.
2. 必须要提供一个公开的空参构造方法.
3. 私有属性必须要提供对应的 setters & getters
常用方法 :
1. setProperty(bean, name, value);
2. getProperty(bean, name);
3. populate(bean, Map<String, Object>); map 参数是 `服务器` 提供. request.getParameterMap();
方法一 : setProperty 设置属性数值
@Test
public void test1() throws Exception {
// 方法一 : setProperty
Class<?> cls = Class.forName("cn.panda.bean.Student");
Object obj = cls.newInstance();
// 使用 BeanUtils 工具类完成反射对象中数据的注入
BeanUtils.setProperty(obj, "name", "Jack");
BeanUtils.setProperty(obj, "age", 18);
BeanUtils.setProperty(obj, "gender", '男');
BeanUtils.setProperty(obj, "score", 90.5f);
System.out.println(obj);
}
方法二 : getProperty 获取属性数值
@Test
public void test2() throws Exception {
Object obj = new Student("张三", 30, '男', 60.5f);
// 获取数据
String name = BeanUtils.getProperty(obj, "name");
String age = BeanUtils.getProperty(obj, "age");
String gender = BeanUtils.getProperty(obj, "gender");
String score = BeanUtils.getProperty(obj, "score");
System.out.println(name + " : " + age + " : " + gender + " : " + score);
}
方法三 : populate 给对象 `注入` 数据
package cn.panda.demo2;
import java.util.HashMap;
import java.util.Map;
public class Request {
public Map<String, Object> getParameterMap() { 模拟方法
Map<String, Object> map = new HashMap<String, Object>();
map.put("name", "渣渣辉");
map.put("age", 20);
map.put("gender", '女');
map.put("score", 100f);
return map;
}
}
@Test
public void test3() throws Exception {
Class<?> cls = Class.forName("cn.panda.bean.Student");
Object bean = cls.newInstance();
Request request = new Request();
BeanUtils.populate(bean, request.getParameterMap()); // request.getParameterMap();
System.out.println(bean);
}
Populate(bean,map)方法的原理是,通过遍历map拿到每一个属性的key和value,其中key是属性名,value是属性值,
动态地为bean类注入数据,在程序运行中,反射获取bean类Class字节码文件cls,,通过cls对属性名key进行反射获取属性对象,对属性对象赋值值(bean的实例对象,value)
1. 综合练习一 :
xml文件 :
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean className="cn.panda.bean.Employee">
<property name="id" value="1"></property>
<property name="name" value="张三"></property>
<property name="salary" value="20000.00"></property>
</bean>
<bean className="cn.panda.bean.Student">
<property name="name" value="翠花"></property>
<property name="age" value="18"></property>
<property name="gender" value="女"></property>
<property name="score" value="99.5f"></property>
</bean>
</beans>
package cn.panda.demo3;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.beanutils.BeanUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
public class TestPractice1 {
public static void main(String[] args) throws Exception {
// 1. SAXReader
SAXReader saxReader = new SAXReader();
// 2. read -> Docuement
Document document = saxReader.read("bean.xml");
// 3. root
Element root = document.getRootElement();
// 4. bean 标签
List<Element> beanElements = root.elements();
List<Object> list = new ArrayList<Object>();
// 5. 遍历
for (Element bean : beanElements) {
// 6. 取出属性数值
String className = bean.attributeValue("className");
// System.out.println(className);
// 7. 反射创建对象
Class<?> cls = Class.forName(className);
Object obj = cls.newInstance();
// 8. elements
List<Element> propElements = bean.elements();
// 9. 遍历
for (Element prop : propElements) {
// 10. 取出 name 和 value 属性
String name = prop.attributeValue("name");
String value = prop.attributeValue("value");
// System.out.println(name + " = " + value);
// 11. BeanUtils 注入属性数据
BeanUtils.setProperty(obj, name, value);
}
// 12. 存储反射创建的对象
list.add(obj);
}
// 查看
for (Object obj : list) {
System.out.println(obj);
}
}
}
2. 综合练习二 :
xml 文件 :
<?xml version="1.0" encoding="UTF-8"?>
<students>
<student stuNo="it001">
<name>章子怡</name>
<age>20</age>
<gender>女</gender>
<score>99</score>
</student>
<student stuNo="it002">
<name>汪峰</name>
<age>22</age>
<gender>男</gender>
<score>100</score>
</student>
<student stuNo="it003">
<name>高圆圆</name>
<age>18</age>
<gender>女</gender>
<score>98</score>
</student>
</students>
package cn.panda.demo3;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.beanutils.BeanUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
public class TestPractice2 {
public static void main(String[] args) throws Exception {
// 1. SAXReader
SAXReader saxReader = new SAXReader();
// 2. read -> Document
Document document = saxReader.read("students.xml");
// 3. rootElemenet
Element root = document.getRootElement();
// 4. 子标签集合
List<Element> stuElements = root.elements();
List<Object> list = new ArrayList<Object>();
// 5. 遍历
for (Element e : stuElements) {
// 6. 获取 stuNo 属性
String stuNo = e.attributeValue("stuNo");
// 创建一个 Student 对象
Object obj = new Student();
BeanUtils.setProperty(obj, "stuNo", stuNo);
// 7. elements 获取所有的子标签集合 (name, age, gender, score)
List<Element> elements = e.elements();
// 8. 遍历
for (Element prop : elements) {
// 9. 获取属性名和属性名
String name = prop.getName();
String text = prop.getText();
BeanUtils.setProperty(obj, name, text);
// System.out.println(name + " = " + text);
}
// 存储对象
list.add(obj);
}
// 遍历集合, 查看对象
for (Object obj : list) {
System.out.println(obj);
}
}
}