[Java基础]面向对象
快速入门
计算机的核心作用就是处理数据, 变量用来存储单个数据, 数组用来储存一批数据, 对象用来存储一类数据
什么是对象?
对象就是一种特殊的数据结构, 用户储存一类数据, 在java中万物皆对象
面相对象编程的好处?
更加符合人类思维习惯
类和实例对象
在java中必须先设计类, 才能根据类创建对象
- 类是创建对象的模板, 对象是类的具体实例
- java的核心是面向对象, 面向对象的核心是设计类
- 只要把类设计好, 就可以创建对象, 使用对象的数据和方法, 就可以完成任何需求
基础语法
- 成员变量
- 类中的变量称为成员变量, 类中的方法称为成员方法
- 完整定义格式:
- 一般无需指定初始值, 存在默认值
- 注意事项
- 命名规范: 类名称首字母大写, 满足驼峰模式
- 一个代码文件中, 可以写多个class类
- 但只能有一个用public修饰, public修饰的类名必须是java代码的文件名
- 实际开发中, 建议一个文件只定义一个class类
// 一个代码文件中, 可以写多个class类
class Teacher {
}
// 建议一个文件只定义一个class类
class Student {
}
// 一个代码文件中, 只能有一个类被public修饰
// public修饰的类必须与文件名一致
public class People {
}
执行原理
执行流程
- java程序运行在jvm虚拟机中
- jvm虚拟机把内存划分为3块, 方法区, 栈内存, 堆内存
- jvm虚拟机执行代码时, 首先把Test启动类提取到方法区中执行
- 然后把Test启动类的main方法放到栈内存中运行
- main方法中使用了Cart类, 虚拟机把Cart类提取到方法区
- new Cart时,会在堆内存中开辟空间, 存放汽车对象, 汽车象会有一个类的地址. 指向方法区的汽车类
- 存放汽车对象的变量地址会被赋值给左侧的c1变量, 对象创建完成
- 对象变量c1通过变量地址, 找到堆内存中的汽车对象, 再找到对象的name属性, 完成属性赋值
注意事项:
- 对象与对象之间的数据不会相互影响, 除非多个变量指向同一个对象
- 如果某个对象没有变量引用它, 该对象无法被操作,会被视为垃圾对象, 垃圾对象会被垃圾回收机制清除
类和对象是什么
- 在内存的角度, 类就是一个文件, 对象是一种数据结构, 类的作用是约束对象, 对象的作用是存储数据
- 从设计的角度, 类是一张设计图, 对象是具体的产品
- 在运行的角度, 类会被虚拟机加载到方法区, 对象是堆内存中的一块空间
构造器
什么是构造器?
构造器是类的五大成分之一, 构造器是一个特殊的方法, 构造器的名字必须与类名一致, 且没有返回值
public class Car {
//无参构造器
public Car() {
}
//有参构造器
public Car(String n) {
}
}
构造器的作用?
构造器的作用就是创建对象并返回对象的地址
public class Car {
//无参构造器
public Car() {
System.out.println("无参构造器执行了");
}
}
// 每次创建对象都会调用构造器
Car c1 = new Car() // 参构造器执行了
- 使用new关键字创建对象时, 对象就会调用构造器, 完成对象的创建, 并返回该对象的地址
构造器的分类
构造器可以分为 有参构造器 和 无参构造器
public class Car {
//成员变量
private String name;
//无参构造器
public Car() {
}
//有参构造器
public Car(String n) {
this.name = n;
}
}
// 调用无参构造器创建对象
Car c1 = new Car()
c2.name; // null
// 调用有参构造器创建对象并赋值
Car c2 = new Car("奔驰")
c2.name; // "奔驰"
- 无参构造器: 创建对象, 里面的数据都是默认值
- 有参构造器: 创建对象, 同时为对象的属性赋值
构造器的特点
类在定义时, java会为类自动生成无参构造器
// 类默认就自带无参构造器
public class Car {
}
Car c1 = new Car()
// 一旦声明了有参构造器, 就要主动声明无参构造器
public class Car {
//主动声明无参构造器
public Car() {
}
//一旦声明了有参构造器
public Car(String n) {
}
}
Car c1 = new Car()
Car c1 = new Car(100)
- 如果类中定义了有参构造器, java就不会自动生成无参构造器了
- 技巧: 如果手动声明了有参构造器, 一定要主动声明无参构造器
this关键字
this就是一个变量, 指向当前对象的地址, 可以用在构造器和成员方法中
public class Car {
// this在构造器中使用
public Car() {
// c1调用构造器, 所以this指向c1
sout("this1:" + this) // 打印: com.itheima.thisdemo.Studentb4c966a
}
// this在成员方法中使用
public void run() {
// c1调用成员方法, 所以this指向c1
sout("this2:" + this) // 打印: com.itheima.thisdemo.Studentb4c966a
}
}
// 调用无参构造器
Car c1 = new Car()
// 调用成员方法
c1.run()
// 打印c1的地址
sout("c1:" + c1) // 打印: com.itheima.thisdemo.Studentb4c966a
- this关键字指向当前类的调用者, 用来访问该对象的成员变量和成员方法
- 技巧: 哪个对象调用方法, this就指向哪个对象
this的作用
可以避免对象的成员变量与方法内部变量名的冲突
// 说明: 功能可以实现, 但是 n 并不是合适的变量名
public class Car {
// 成员变量
String name;
// 成员方法
public void goWith(String n) {
sout(name + "正在和" n + "比赛加速!")
}
}
// 调用无参构造器
Car c1 = new Car()
c1.name = "宝马"
// 调用成员方法
c1.rungoWith("奔驰"); // 宝马正在和奔驰比赛加速!
// 说明: 使用name变量接收参数, 使用this.name访问对象属性, 代码很优雅
public class Car {
// 成员变量
String name;
// 成员方法
public void goWith(String name) {
sout(this.name + "正在和" name + "比赛加速!")
}
}
// 调用无参构造器
Car c1 = new Car()
c1.name = "宝马"
// 调用成员方法
c1.rungoWith("奔驰"); // 宝马正在和奔驰比赛加速!
封装性
- 三大特性
面向对象的三大特性: 封装, 继承, 多态
- 封装的概念
封装是设计对象的原则, 设计对象时, 要把数据以及数据的相关方法设计在一起
- 封装的好处
让编程变得简单, 有什么事情, 找对象调方法就行了
- 封装的原则
合理隐藏: 成员变量全部隐藏, 提供get/set方法访问数据
合理暴露: 成员方法根据是否有用对外暴露
- 修饰符
公开成员: public修饰符, 公开成员可以通过对象直接访问
私有成员: private修饰符, 私有成员无法通过对象访问, 只能在类的内部访问
public class Student {
// 成员变量私有
private double score;
// 成员方法按需暴漏
public void setScore(double score) {
this.score = score;
}
// 通过方法操作数据
public double getScore(double score) {
return score;
}
}
实体类
javaBean也称为实体类, 是一种专门保存数据的java类
要求
- 成员变量必须私有(private), 提供getXX/setXX方法
- 必须提供无参构造器, 有参构造器可选
好处
- 实际开发中数据和数据的处理都会很多, 如果都混在一起, 代码的可读性和可维护性都很差
- 所以流行的开发方式就是数据和数据处理相分离, 所以要用实体类保存数据
补充
- 选中代码后, 右键快捷生成gte/set方法
- 快速生成有参构造器
- 快速生成无参构造器
static关键字
static是静态的意思, 也就是静态修饰符, 可以用来修饰成员变量, 成员方法
静态变量
使用static修饰的成员变量就是静态变量
// 定义
public class User {
// 静态变量
static String name;
// 实例变量
int age;
}
// 访问方式1(推荐)
// 通过类名访问: 类名.静态成员变量
User.name;
// 访问方式2(不推荐)
// 通过对象访问: 对象.静态成员变量
User u = new User();
// 访问静态变量
u.name;
// 访问实例变量
u.age;
- static修饰的成员变量就是静态变量(类变量)
- 静态变量属于类, 可以被类和类的所有对象访问
- 推荐通过类名访问静态变量, 因为用过对象访问实例变量, 并不直观
- 无static修饰的成员变量就是实例变量
- 实例变量是属于每个对象的
- 只能通过实例对象访问, 且每个对象的信息不同
执行原理
- 静态变量只会跟随类加载一次, 内存中只有一份, 可以被类和类的所有对象共享
- 实例变量属于对象, 每个对象中都有一份, 只能用对象访问
应用场景
如果某个数据只需要一份, 且希望能够被共享, 该数据就应该定义为静态变量
// 系统启动后, 需要用户类记住自己创建了多少个对象
public class User {
// 类变量
public static int number;
// 构造器
public User() {
// 访问自己类中的静态变量, 可以省略类名
// User.number++;
number++;
}
}
// 创建对象
new User(); // 每次创建对象, 都会调用构造器
new User();
sout(User.number); // 2
- 访问自己类中的静态变量, 类名可以省略不写
- 在类中访问其他类的静态变量, 必须带类名访问
静态方法
使用static修饰的成员方法就是静态方法, 静态方法归属于类
// 定义
public class User {
// 实例方法
public void run() {
sout("好好学习.天天向上")
}
// 静态方法
public static int getMax(int a, int b) {
return a > b ? a : b
}
}
// 访问方式1(推荐)
// 通过类名访问: 类名.静态成员方法
User.getMax();
// 访问方式2(不推荐)
// 通过对象访问: 对象.静态成员方法
User u = new User();
// 访问静态方法
u.getMax();
// 访问实例方法
u.run();
- 静态成员方法(static修饰, 属于类), 建议使用类名触发, 也可以用对象触发(不直观)
- 实例成员方法(无static修饰, 属于对象),只能用对象触发
最佳实践
- 如果该方法是对象的行为, 且需要访问对象的数据, 则该方法必须声明成实例方法
- 如果该方法只是执行一个功能, 且不需要访问对象的数据, 则可以声明成静态方法
注意事项
public class Test {
public static String name;
public int age;
public static void main(String[] args) {
}
// 1,静态方法中可以访问静态成员, 不可以直接访问实例成员
public static void printHello() {
sout(name); // 本质是Test.name
sout(age); // 本质是Test.age, 所以报错, 实例成员必须通过对象访问
sout(this); // 报错, this只能代表对象, 静态方法通过类名掉用,找不到对象
}
// 2,实例方法中可以访问静态成员, 也可以访问实例成员
public static void printHello() {
sout(name); // 本质是Test.name
sout(age); // 本质是this.age, 通过对象访问实例成员没毛病
sout(this); // 哪个对象调用方法, this就指向哪个对象
}
}
- 静态方法中可以访问静态成员, 不可以直接访问实例成员
- 实例方法中可以访问静态成员, 也可以访问实例成员
- 实例方法中可以使用this关键字, 静态方法中不可以使用this关键字
- 上面的语法能看懂就可以了, 不要思考有什么作用, 想多了会乱, 认识语法就行
搞懂main方法
main方法就是一个静态方法
public class Test2 {
public static void main(String[] args) {
Student s1 = new Student();
s1.name = "林青霞";
s1.math = 89;
s1.chinese = 76;
s1.printAllScore();
s1.printAllScore();
}
}
- 执行原理: 使用java命令运行程序时, 虚拟机是通过Test(类名)访问main方法, 最终启动程序
工具类
工具类是静态方法最常见的应用
public class XxxUtil {
// 方法1
public static void xxx() {
...
}
// 方法2
public static boolean xxx(String email) {
...
}
// 方法3
public static String xxx(int n) {
...
}
// 构造器私有
private XxxUtil() { }
}
// 通过类名调用静态方法
XxxUtil.xxx()
- 工具类就是一个类中只定义静态方法, 每个方法完成一个功能, 这个类就是工具类
- 调用方便: 如果使用实例方法, 还要new出实例, 麻烦而且浪费内存
- 代码复用: 一次编写, 处处可用
- 建议: 工具类不需要创建对象, 建议把工具类的构造器进行私有
继承性
继承就是用extends关键字, 让一个类和另一个建立父子关系
// Student称为子类
// People称为父类
public class Student extends People {
}
示例
子类继承父类后, 就得到了父类的非私有成员(成员变量/成员方法)
public class People {
private String name;
public String getName() {
retuen name;
}
public void setName(String name) {
this.name = name;
}
}
public class Teacher extends People {
private String skill; //技能
public String getSkill() {
retuen skill;
}
public void setSkill(String skill) {
this.skill = skill;
}
}
public class Stuted extends People {
private String exam; // 作业
public String getExam() {
retuen exam;
}
public void setName(String exam) {
this.exam = exam;
}
}
public class Test {
public static void main(String[] age) {
// 子类可以继承父类的非私有方法和属性
// 说明: People类中的name属性是私有的, 所以子类并不能继承这个属性
// 但是getName()和setName()方法是公开的, 所以就可以读取和设置该属性
Teacher t = new Teacher();
t.setName("王六");
t.setSkill("java, 大数据");
t.getName(); // 王六
t.getSkill(); // java, 大数据
}
}
- 好处: 继承的好处就是提高代码的复用性
- 规范
- 子类相同的特性(公共属性/公共方法)放在父类中定义
- 子类独有的属性和方法放在子类中定义
继承的原理
子类的对象是由子类和父类共同决定的
// 父类
public class Student {
String name;
}
// 子类
class Student {
String age;
}
// 子类继承父类后
class Student extends People {
String name;
String age;
}
权限修饰符
权限修饰符的作用就是 限制类中的成员 (成员方法/成员变量/构造器), 能够被访问的范围
- 访问权限从小到大是: private(私有)< 缺省(不写) < protected(受限制) < public(公开)
- 一般建议:
- 需要私有的成员使用private修饰
- 需要公开的成员使用public修饰
- 需要在子孙类中访问使用protected修饰符, 较少使用
继承的特点
- java是单继承模式: 一个类只能继承一个直接父类
class A {
public void method() {
sout("复习数学!")
}
}
class B {
public void method() {
sout("复习语文!")
}
}
// 语法错误, 不支持多继承
public class c extends A,B {
}
// 原因: 是复习数学还是复习语文?
// 如果支持多继承, 那么一旦出现冲突, 以谁为准是个很大的问题
C c = new C();
c.method();
- java不支持多继承, 但是支持多层继承
public class People extends Zoon {
}
class Student extends People {
}
- java中所有的类都是Object类的子类, 要么直接继承, 要么间接继承
// 默认继承Object
public class Movie extends Object {
};
Moive m = new Moive();
m.hashCode(); // 所有类都能使用Object类中的方法
- 原因1: Java的继承体系模仿现实世界, 人类共同的祖先是非洲直立猿, java中所有的类祖先就是Object
- 原因2: 所有对象的通用方法可以定义在Object中, 子类可以轻松的使用
- 子类不能继承父类的构造器, 因为子类有自己的构造器
- 子类可以直接使用父类的静态成员(共享), 但是共享不等于继承
继承后的变化
1.子类访问成员的原则
在子类方法中访问成员(成员变量/成员方法)遵循就进原则
class A {
String name = "国强";
}
class B extends A {
String name = "军军";
public void run() {
String name = "小红";
sout(name); // 小红, 就近原则
sout(this.name); // 军军, this指向实例对象
sout(super.name); // 国强, super指向父类的成员
}
}
B b = new B();
b.run();
- 先在子类局部范围找, 然后在子类成员范围找, 然后在父类范围找, 找不到就报错
- 如果子父类中出现了重名的成员, 会优先使用子类的(更近)
- 如果一定要在子类中使用父类的成员, 可以通过super关键字,指定访问父类的成员
- 格式: super.父类成员变量/父类成员方法
- 也可以通过this关键字, 指定访问子类的成员 (没用, 因为默认就访问子类的成员)
- 格式: this.成员变量/成员方法
2.子类的方法重写
当子类觉得父类中的某个方法不好用, 子类可以重写一个名称和参数列表相同的方法, 去覆盖父类的方法
class Animal {
public void cry() {
sout("动物都会叫");
}
}
class Cat extends Animal {
//方法重写: 方法的名称和形参列表必须与被重写方法保持一致
@Override // 方法重写的标志, 可以检查重写的格式, 不符合重写要求会报错
public void cry() {
sout("喵喵喵");
}
}
public class Test {
public static void main(String[] ages) {
Cat cat = new Cat();
// 重写后, 方法的访问就会遵循就近原则
cat.crt(); // 喵喵喵
}
}
方法重写的注意事项
- 私有方法不能被继承, 所以不能被重写
- 静态方法归父类持有, 所以不能被重写
- 子类重写父类方法, 访问权限必须等于或大于父类该方法的权限
- 重写的方法的返回值, 必须与被重写方法的返回值一致 或者 范围更小
- 以上了解即可, 实际开中遵循原则: 声明不变, 重新实现
方法重写的应用
子类重写Object类的toString方法, 使其返回对象内容, 而不是地址
public class Movie {
private int id;
private String name;
@Override
public String toString() {
return "Movie{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public Movie(int id, String name) {
this.id = id;
this.name = name;
}
}
Movie m = new Movie(4,"西红柿首富");
// 直接输出对象, 默认会调用Object的toString()方法, 返回对象的地址信息
// 完整写法: sout(m.toString());
sout(m); // com.itheima.dome@b4c77a
// 方法重写后返回对象内容, 便于我们观察对象
sout(m); // { id=4, name="西红柿首富" }
- 重写toString方法经常使用, 可以快捷生成
- 快捷生成: 右键->Generate->toString 快速生成
- 快速生成: toString + 回车 + 回车
3.子类构造器的特点
子类中所有的构造器, 默认都会先调用父类的无参构造器, 再执行自己
class Fu {
private String name;
public Fu() {
sout("父类无参构造器执行了")
}
public Fu(String name) {
sout("父类无参构造器执行了")
this.name = name
}
}
class Zi extends Fu {
public Zi() {
// 3. 原因: 子类默认会调用父类的无参构造器, 写不写都存在
// super()
// 4. 如果父类没有无参构造器, 必须手动调用父类的有参构造器, 否则代码报错
// super("张三")
sout("子类无参构造器执行了")
}
}
public class Test {
public static void main(String[] aegs) {
Zi z = new Zi();
// 1. 控制台:
// 先打印 -> 父类无参构造器执行了
// 再打印 -> 子类无参构造器执行了
// 2. 现象: 子类的构造器一定先调用父类的构造器, 再执行自己的构造器
}
}
- 子类构造器必须先调用父类构造器的原因:
- 在继承模式下, 子类对象是由子类和父类共同决定的
- 子类对象在初始化的时候, 会用到父类中的数据, 如果父类没有初始化, 子类就无法使用父类的数据
- 所以, 子类初始化之前, 一定会调用父类的构造器, 完成父类的初始化
- 然后再调用自己的构造器, 最终完成子类对象的初始化
- 子类如何调用父类的构造器
- 默认情况下, 子类构造器的第一行代码都是super() , 写不写都有, 他会调用父类的无参数构造器
- 如果父类没有无参构造器, 则会报错, 我们需要在子类调用父类的有参构造器, 语法 super(参数)
- 子类构造器和父类构造器的作用
- 在继承模式下, 子类对象是由子类数据和父类数据共同组成的,
- 父类的构造器为父类这部分的数据赋值
- 子类的构造器为子类这部分的数据赋值
- 最终由子类构造器, 把完整的子类对象构建出来
- 这样设计的目的:
- 先运行父类构造器, 再运行子类构造器, 从代码逻辑上确保子类对象的初始化过程就绝对可靠,
- 就能保证子类对象数据的完整性
运行原理
- 子类构造器可以通过调用父类构造器,把对象中包含父类这部分的数据先初始化赋值
- 再回来把对象里包含子类这部分的数据也进行初始化赋值。
4.调用兄弟构造器
任意构造器中, 都可以通过 this(...) 调用该类的其他构造器
public class Student {
private String name;
private char sex;
private int age;
private String schoolName;
// 有参构造器1
public Student(String name, char sex, int age) {
// 调用兄弟构造器
this(name, sex, 22, '清华大学');
}
// 有参构造器2
public Student(String name, char sex, int age, String schoolName) {
this.name = name;
this.sex = sex;
this.age = age;
this.schoolName = schoolName;
}
}
// 初始化学生对象
Student s = new Student("张三",'男')
- 执行顺序: 子类通过 this(...) 调用本类的其他构造器, 其他构造器通过 super(...) 去调用父类的构造器
- 注意事项: super(...) 和 this(...) 都只能放在构造器的第一行, 所以不能同时使用
this和super
this代表本类对象的引用, super代表父类存储空间的标识
多态性
认识多态
多态就是对象可以有多种形态, 是继承/实现情况下的一种现象, 表现为: 对象多态和行为多态
// 人类
public calss People {
public void run() {
sout("人会跑~")
}
}
// 老师类
calss Teacher extedns People {
public void run() {
sout("老师跑的慢~")
}
}
// 学生类
calss Student extedns People {
public void run() {
sout("学生跑的快~")
}
}
// 认识多态
People p2 = new Teacher();
p2.run(); // 老师跑的慢~
People p1 = new Student();
p1.run(); // 学生跑的快~
- 多态是面向对象的一种设计思想, 源于对现实世界的模仿:
- 老师类和学生类都继承人类,
- 人类会跑, 老师和学生就会跑(因为继承),
- 但是老师和学生跑起来是有区别的(方法重写)
- 对象多态: 就是同一个对象可以有不同的形态
- 学生和老师都继承人类这个对象
- 人类就包含了学生和老师这两种不同的形态
- 行为多态: 一个对象的行为可以有不同的表现
- 学生和老师都继承人类这个对象
- 老师跑起来和学生跑起来有不同的表现
- 多态的前提
- 有继承/实现关系;
- 存在父类引用子类对象;
- 存在方法重写 (多态侧重行为多态)
常见形式
多态的常见形式就是 父类类型接收子类对象, 语法: 父类类型 对象名称 = new 子类构造器
public class Prople {
public String name = "父类的名称"
public void run() {
sout("人可以跑")
}
}
public class Teacher extends Prople {
public String name = "老师的名称"
public void run() {
sout("老师跑的气喘吁吁")
}
}
public class Student extends Prople {
public String name = "学生的名称"
public void run() {
sout("学生跑的飞快")
}
}
public class Test {
public static void main(String[] ages) {
// 对象多态: 人对象可以指向学生对象,也可以指向老师对象
People p1 = new Student();
People p2 = new Teacher();
// 行为多态: 同样调用run方法,执行结果不同
p1.run(); // 学生跑的飞快
p2.run(); // 老师跑的气喘吁吁
// 成员变量不存在多态
p1.name; // 父类的名称
p1.name; // 父类的名称
}
}
运行特点
- 方法调用: 编译看左边(People), 运行看右边(Student();)
- 变量调用: 编译看左边(People), 运行也看左边(People)
- 重要结论: 多态是指对象或行为的多态, java中的属性(成员变量)不谈多态
多态的好处
1:在多态形势下, 右边对象是解耦合的, 更便于扩展和维护
// 可以方便的切换对象
// prople p1 = new Teacher();
prople p1 = new Student();
p1.run()
2:定义方法时, 使用父类类型的形参, 可以接受一切子类对象, 扩展性更强
Student s = new Student();
go(s);
Teacher t = new Teacher();
go(t);
publci static void go(People p) {
}
多态的问题
多态下不能使用子类的独有功能
public class Prople {
public void run() {
sout("人可以跑")
}
}
public class Teacher extends Prople {
public void run() {
sout("老师跑的气喘吁吁")
}
public void sayHi() {
sout("老师需要讲课")
}
}
public class Test {
public static void main(String[] ages) {
People p1 = new Teacher();
p1.run(); // 老师跑的气喘吁吁
p1.sayHi(); // 报错, 因为编译看左边, 左边的父类中没有sayHi这个方法
}
}
解决方法
把父类类型强转成子类类型, 解决多态下无法使用子类独有功能的问题
public class Test {
public static void main(String[] ages) {
People p1 = new Teacher();
// 多态下,无法调用子类的独有功能
p1.sayHi(); // 报错
// 强制类型转换
Teacher s1 = (Teacher) p1;
// 类型强转后, 就可以调用子类的独有方法了
p1.sayHi(); //"老师需要讲课"
// 隐患: 只要由继承关系就可以强制转换, 编译阶段不会报错
// 把老师s1 强转为 学生类型, 明显是不对的, 但是编译通过
Student sd1 = (Student) s1;
// 类型判断, 防止类型转换错误
if(p1 instanceof Teacher) {
Teacher s1 = (Teacher) p1;
p1.sayHi();
}
}
}
- 只要存在继承/实现关系, 就可以在编译阶段进行强制类型转换, 编译阶段不会报错
- 运行时, 如果发现强转的类型和对象的真实类型不符, 就会报类型转换异常(ClassCastException)的错误
- 官方建议, 强制类型转换前先 使用 instanceof 关键字, 判断当前对象的真实类型
final关键字
final 关键字是最终的意思, 可以修饰 类/方法/变量
- 修饰类: 该类就是最终类, 特点是不能被继承(类似绝育)
// 工具类建议使用final关键字修饰,
// 因为工具类不需要创建对象, 更不需要被继承
public final class CodeUtil {
// 工具类不需要创建对象, 建议私有化构造器
private CodeUtil() {}
// 工具类中全是静态方法
public static String getCode(int n) {
... ...
}
}
- 修饰方法: 该方法就是最终方法, 特点是不能被重写
public class Dome {
// 工具类中全是静态方法
public final void show() {
sout("show方法执行了")
}
}
class Dome2 extends Dome {
// 报错, 最终方法不能被重写
@Override
public void show() {
sout("最终方法不能重写吗?")
}
}
- 修饰变量: 该变量必须赋值且不能二次赋值
/**
java中的变量有几类?
a. 成员变量
->静态成员变量
->实例成员变量
b. 局部变量
*/
public class Dome {
// 2.final修饰静态成员变量, 就被称为常量
public static final String SCHOOL_NAME = "清华大学";
// 3.final修饰实例成员变量, 没有意义,
// 所有的对象年龄都是18, 且不能改
private final int age = 18;
public static void main(String[] args) {
// 1.final修饰局部变量, 可以防止数据被意外修改
final double rate = 3.1415926;
rate = 3.14; // 报错,不能被二次赋值
}
}
- final修饰基本类型的变量: 该变量存储的数据不能被改变
- final修饰引用类型的变量: 该变量存储的地址不能被改变, 但是对象里面的值可以改变
常量
使用 public static final 修饰的成员变量就是常量, 必须要有初始值, 常用于记录系统的配置信息
public class Constant {
public static final String SCHOOL_NAME = "清华大学";
public static final String SYSTEM_VERSION = "v1.0"
}
- 命名规范: 全大写英文, 多个单词使用下划线连接
- 优势:
- 实现软编码, 代码可读性更好,可维护性更好
- 程序编译后,常量会被"宏替换", 出现常量的地方全部会被替换为字面量,所以性能不受影响
枚举类
枚举是java中一种特殊的类, 专门用来做信息分类, 可读性好, 入参约束谨慎, 代码优雅
语法
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
- 枚举类中的第一行,只能写枚举类的对象名称,且要用逗号隔开。
- 这些名称,本质是常量,每个常量都记住了枚举类的一个对象。
反编译后
- 手动反编译: javap .\A.class
- 枚举类都是继承了枚举类型: java.lang.Enum
- 枚举都是最终类, 不可以被继承
- 枚举类的构造器都是私有的, 所以对外不能创建对象
- 枚举类的第一行只能罗列一些名称, 这些名称都是常量, 并且每个常量记住的都是枚举类的一个对象
- 枚举类中, 从第二行开始, 可以定义类的其他各种成员
- 枚举类相当于是多例模式
- 枚举会从 java.lang.Enum 类中继承一些方法
public enum A {
X, Y, Z;
}
A a1 = A.X;
sout(a1); // X, 默认返回枚举的名字, 而不是枚举对象的地址, 因为枚举类默认重写了toString()方法
sout(a1.name); // X,拿到枚举对象的名字
sout(a1.ordinal()); // 0,拿到枚举对象的索引
// 可能会用到的方法
A[] aa1 = A.values(); // 拿到枚举类中的全部枚举对象
A aa2 = A.valueOf("X"); // 拿到指定名称的枚举对象
应用场景
可以使用枚举实现单例模式
public enum C {
x; //单例
}
信息分类: 枚举和常量都是用来表示一组信息, 然后作为参数进行传输
class Constan {
public static final int UP = 0; // 上
public static final int DOWN = 1; // 下
public static final int LEFT = 2; // 左
public static final int RIGHT = 3; // 右
}
public class Test {
public static void main(String[] args) {
// 使用常量做信息标识和分类挺好, 只是入参不受约束
move(Constan.UP);
move(10086);
}
public static void move(int direction) {
switch(direction) {
case Constan.UP:
sout("向上移动");
break;
case Constan.DOWN:
sout("向下移动");
break;
case Constan.LEFT:
sout("向左移动");
break;
case Constan.RIGHT:
sout("向右移动");
break;
default:
sout("参数有误");
}
}
}
enum Direction {
UP, DOWN, LEFT, RIGHT;
}
public class Test {
public static void main(String[] args) {
// 使用常量做信息标识和分类, 代码提示和约束更好
move(Direction.UP);
}
public static void move(Direction direction) {
switch(direction) {
case Direction.UP:
sout("向上移动");
break;
case Direction.DOWN:
sout("向下移动");
break;
case Direction.LEFT:
sout("向左移动");
break;
case Direction.RIGHT:
sout("向右移动");
break;
}
}
}
- 枚举相对于常量, 限制性更好,
- 常量相对于枚举, 灵活性更好,
- 两者的使用场景相似
抽象类
abstract关键字是抽象的意思, 可以修饰类, 成员方法
// 1.抽象类
// abstract修饰的类称为抽象类
public abstract class A {
// 抽象方法
// 1.1.abstract修饰的方法称为抽象方法
// 1.2.抽象方法只有方法签名, 不能声明方法体
public abstract void show();
}
// 2.抽象类不能创建对象
// A a = new A; // 报错
// 3.一个类继承抽象类, 必须重写完抽象类的全部抽象方法, 否则这个类也要定义成抽象类
public class B extends A {
@Override
public void show() {
sout("子类重写父类方法")
}
}
B b = new B();
b.show(); // "子类重写父类方法"
- 一个类中如果定义了抽象方法,这个类必须声明为抽象类, 否则报错
- 类该有的成员(成员变量/方法/构造器), 抽象类都可以有
- 特点: 抽象类可以定义抽象方法, 但是不能创建对象
- 作用: 抽象类的使命就是被子类继承并实现抽象方法
- abstract不能修饰变量, 代码块, 构造器
应用场景
父类知道每个子类都要做某个行为, 但每个子类要做的情况不一样
- 父类就可以定义抽象方法, 交给子类去重写实现,
- 设计这样的抽象类, 就是为了更好的支持多态
// 动物类
public class abstract Animal {
// 每个动物都会叫
public abstract void cry();
}
// 猫类
// 猫类继承动物类
class Cat extends Animal {
// 动物类是抽象类, 所以必须重写父类全部抽象方法
@Override
public void cry() {
sout("猫喵喵的叫");
}
}
// 狗类
// 狗类继承动物类
class Dog extends Animal {
// 动物类是抽象类, 所以必须重写父类全部抽象方法
@Override
public void cry() {
sout("狗汪汪的叫");
}
}
// 不使用抽象类, 也可以完成需求
// 但是使用抽象类, 代码就会非常优雅, 符合最佳实践
Animal a1 = new Dog();
a.cry(); // 狗汪汪的叫
Animal a2 = new Cat();
a.cry(); // 猫喵喵的叫
扩展
- final和abstract是互斥关系
- 抽象类的目的就是让子类继承, final定义的类不能被继承
- 抽象方法的目的就是让子类重写, final定义的方法子类不能重写
- 使用抽象类实现 [模版方法] 设计模式是也能最佳实践
- 模板设计方法移步 [设计模式] 章节查看
接口类
传统接口
接口是一种规范, 在java中, 使用 interface 关键字定义接口, 可以理解为一种特殊的类
public interface A {
// 成员变量(默认就是常量)
String SCHOOL_NAME = "程序员";
// 完整形式
//public static final String SCHOOL_NAME = "程序员";
// 成员方法(默认就是抽象方法)
void test();
// 完整形式
// public abstract void test();
}
- JDK8之前的接口中只能定义成员变量(常量) 和 成员方法(抽象方法)
- 接口中的成员都是public修饰的, 写不写都是
- 接口方法默认被public修饰的原因: 接口是需要被类实现的, 所以接口方法都是要对外暴漏的
接口不能创建对象, 接口是用来被 类 实现的, 实现接口的类称为实现类
// 实现接口的关键字: implements
public class B implements A {
@Override
void test() {
sout("test...")
}
}
B b = new B;
b.test(); // test...
sout(A.SCHOOL_NAME); // 程序员
- 一个类可以实现多个接口(干爹), 称为接口的多实现
- 一个类实现接口, 必须重写完 全部接口的 全部抽象方法, 否则实现类需要定义为抽象类
接口的好处
弥补了类单继承的不足,一个类可以同时实现多个接口
// 司机接口
interface Driver {}
// 男朋友接口
interface BoyFriend {}
// 人类
class People {}
// 学生类
class Student extends People implements Driver, BoyFriend {}
// 同一个类可以拥有更多角色, 类的功能更强大. 更符合现实
People p = new Student();
Driver d = new Student();
BoyFriend bf = new Student();
面向接口编程, 可以灵活方便的切换各种业务实现
// 司机接口
interface Driver {}
// 男朋友接口
interface BoyFriend {}
// 人类
class People {}
// 学生类
class Student extends People implements Driver, BoyFriend {}
// 老师类
class Teacher extends People implements Driver, BoyFriend {}
// 面向接口编程, 灵活的切换业务实现对象
BoyFriend bf = new Student();
BoyFriend bf = new Teacher();
// 假设业务中已经使用对象, 完成很多功能
bf.go();
bf.say();
bf.look();
- 更详细的案例可查看代码day05-oop/dome3
增强接口
jdk8开始, 接口新增了三种方法, 目的是增强接口的能力, 更便于项目的扩展和维护
1.默认方法
public interface A {
// 默认方法必须使用default修饰
default void run() {
sout("默认方法执行了")
}
}
// 默认方法, 使用实现类的对象调用
class AImpl implements A {}
AImpl a = new AImpl();
a.run();
2.静态方法
public interface A {
// 静态方法必须使用static修饰
static void run() {
sout("静态方法执行了")
}
}
// 静态方法, 只能使用当前接口名来调用
A.run();
3.私有方法
public interface A {
// 私有方法必须使用private修饰
static void run() {
sout("私有方法执行了")
}
default void show() {
// jdk9开始支持, 只能在本类中被其他的默认方法或私有方法调用
run();
}
}
- 这些方法了解即可, java源码中会看到, 但是我们很少会写
- 这写方法默认都会被public修饰
- 使用场景: 实际工资中一个接口可能被几十个类实现, 如果给接口增加一个成员方法, 那么这几十个类都要重写这个新增的方法, 修改成本较高, 此时可以定义静态方法或者默认方法, 减少对其他实现类的影响
接口的多继承
一个接口可以同时继承多个接口, 便于实现类实现多个接口
interface A {
void show1();
}
interface B {
void show2();
}
interface C extends A, B{
void show3();
}
// 相当于D实现了A+B+C
class D implements C {
@Override
public void show1(){ }
@Override
public void show2(){ }
@Override
public void show3(){ }
}
- 类与类: 单维承, 一个类只能继承一个直接父类
- 类与接口: 多实现,一个类可以同时实现多个接口
- 接口与接口: 多维承,一个接口可以同时维承多个接口
使用细节
一个接口继承多个接口, 如果多个接口中存在方法签名冲突, 则此时不支持多继承, 也不支持多实现
interface A {
void show();
}
interface B {
String show();
}
// 原因: 方法冲突时, 实现或继承时,就不知道听谁的了
// 方法冲突,不支持多继承
interface C extends A, B{
}
// 方法冲突,不支持多实现
class D implements A,B {
}
一个类继承了父类, 又同时实现了接口, 父类中和接口中存在同名的默认方法, 实现类会优先使用父类的
interface A {
default void show() {
sout("接口的show方法执行了")
}
}
class B {
public void show() {
sout("父类的show方法执行了")
}
}
class C extends B implements A {
// 如果就要执行干爹的方法, 只能使用方法中转调用[了解]
public void go() {
A.super.show();
}
}
// 就近原则: 亲爹和干爹打架,听亲爹的
C c = new C();
c.show(); // 父类的show方法执行了
// 就要听干爹的[了解]
c.go(); // 接口的show方法执行了
一个类实现了多个接口, 多个接口中存在同名的默认方法, 会冲突, 解决方法就是在实现类中重写该方法
interface A {
default void show() {
sout("A接口的show方法")
}
}
interface B {
default void show() {
sout("B接口的show方法")
}
}
class D implements A,B {
// 方法冲突了, 只能重写[了解]
@Override
public void show() {
// 1. 谁的我也不听了,我自己决定
sout("略略略")
// 2. 听干爹A的
A.super.show()
// 1. 听干爹B的
B.super.show()
}
}
对比一下
抽象类和接口的相同点和不同点
相同点:
1、都是抽象形式,都可以有抽象方法,都不能创建对象。
2、都是派生子类形式: 抽象类是被子类继承使用,接口是被实现类实现。
3、一个类继承抽象类,或者实现接口,都必须重写完他们的抽象方法,否则自己要成为抽象类或者报错!
4、都能支持多态,都能够实现解耦合。
不同点:
1、抽象类中可以定义类的全部普通成员,接口只能定义常量,抽象方法(JDK8新增的三种方式)
2、抽象类只能被类单继承,接口可以被类多实现。
3、一个类继承抽象类就不能再继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)。
4、抽象类体现模板思想: 更利于做父类,实现代码的复用性。[最佳实践]
5、接口更适合做功能的解耦合: 解合性更强更灵活。[最佳实践]
代码块
代码块是类的5大成分之一: 成员变量/构造器/方法/代码块/内部类
在java中, 使用 {} 括起来的代码被称为代码块
- 静态代码块
- 格式: static { }
- 特点: 类加载时自动执行, 由于类只会加载一次, 所以静态代码块也只会执行一次
- 应用: 对类的静态资源进行初始化
public class CodeDemo {
public static String schoolName;
public static String[] cards = new String[3];
// 静态代码块: 对类的静态变量初始化赋值
// 优势: 静态代码块只会执行一次, 可以保证静态资源只初始化一次
static {
sout("静态代码块执行了")
schoolName = "清华大学";
cards[0] = "A";
cards[1] = "B";
cards[2] = "C";
}
public static void main(String[] args) {
sout(schoolName); // 清华大学
sout(Arrays.toString(cards)); ["A","B","C"]
}
}
- 实例代码块(了解)
- 格式: { }
- 特点: 每次创建对象, 都会执行实例代码块, 并在构造器之前执行
- 使用: 和构造器一样, 用来完成对象的初始化
public class CodeDemo {
public String schoolName;
public String[] direction = new String[4];
// 实例代码块: 对实例变量进行初始化赋值
{
sout("实例代码块执行了")
schoolName = "清华大学";
direction[0] = "N";
direction[1] = "S";
direction[2] = "E";
direction[3] = "W";
}
public static void main(String[] args) {
// 每次创建对象, 都会执行实例代码块, 为当前对象的实例变量赋值
CodeDemo c1 = new CodeDemo();
sout(Arrays.toString(c1.direction)); // ["N","S","E","W"]
CodeDemo c2 = new CodeDemo();
sout(Arrays.toString(c2.direction)); // ["N","S","E","W"]
}
}
内部类
如果一个类中定义在另一个类的内部, 这个类就是内部类
- 内部类是类的五大成分之一(成员变量/方法/构造器/内部类/代码块)
- 场景: 当一个类的内部, 包含了一个完成的事物, 且这个事物没必要单独设计时, 就可以设计成内部类
- 内部类通常可以方便访问外部类的成员, 包括私有成员
- 内部类提供了更好的封装性, 可以使用private, protectecd修饰内部类
成员内部类
属于类的一个普通成员, 类似普通的成员变量, 成员方法
// 定义
public class Car {
public static String schoolName = "清华大学";
public int age;
// 1.成员内部类
// 无static修饰, 属于外部类的对象持有
public class Inner {
// 成员内部类可以定义类的所有结构
private String name;
public Inner() {
sout("无参构造器")
}
public Inner(String name) {
this.name = name;
sout("有参构造器")
}
public void show() {
sout("内部类的成员方法")
//3.1 成员内部类中可以直接访问外部类的静态成员和实例成员
sout(schoolName); // 清华大学
sout(age); // 0
//3.2 通过this拿到自己的对象,通过 外部类名.this 拿到外部类的对象
sout(this); //ci对象
sout(Car.this) // car对象
}
}
}
// 2. 成员内部类创建对象的语法
// 外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
Car.Inner ci = new Car().new Inner();
ci.show(); // 内部类的成员方法
// 3.访问成员
// 3.1可以直接访问外部类的实例成员和静态成员
// 3.2可以拿到当前外部类对象, 格式是: 外部类名.this
// 扩展面试题
class People {
private int heartBeat = 100;
public class Heart {
private int heartBeat = 80;
public void show() {
int int heartBeat = 20;
sout(heartBeat); // 20
sout(this.heartBeat); // 80
sout(People.this.heartBeat) // 100
}
}
}
People.Heart heart = new People().new Heart();
heart.show();
静态内部类
用static修饰的内部类就是静态内部类, 属于外部类自己持有, 特点与普通类一致
// 1.定义
public class Car {
public static String schoolName;
private int age; // 实例成员
// 静态内部类: 属于外部类持有
public static class Inner {
// 静态内部类中可以定义所有的类成员
private String name;
public void show() {
//2.1 可以直接访问外部类的静态成员
sout(schoolName); // null
//2.2.不可以直接访问外部类的实例成员
sout(age); // 报错
}
}
}
// 创建对象
Car.Inner ci = new Car.Inner();
ci.show();
// 访问成员
// 2.1.可以直接访问外部类的静态成员
// 2.2.不可以直接访问外部类的实例成员
局部内部类
局部内部类是定义在方法中、代码块中、构造器等执行体中
public class Test {
public static void main(String[] args) {
}
// 成员方法
public static void go() {
// 1.局部内部类
class A { }
// 2.局部内部类
abstract class B { }
// 3.局部内部类
interface C { }
}
}
匿名内部类(重点)
认识
匿名内部类是一种特殊的局部内部类, 匿名是指这个这个类不需要声明名字, 默认有个隐藏的名字
// 1.定义一个抽象类
abstract class Animal {
public abstract void cry();
}
// 2.使用传统方式得到子类对象
class Cat extends Animal {
@Override
public void cry() {
sout("猫喵喵喵")
}
}
Animal a = new Cat();
a.crt(); // 猫喵喵喵
// 3.使用匿名内部类语法
// 把匿名内部类编译成一个子类, 然后立即创建一个子类对象
Animal a = new Animal() {
@Override
public void cry() {
sout("喵喵喵")
}
};
// 直接使用子类
a.cry(); // 猫喵喵喵
- 特点: 匿名内部类本质是一个子类, 并且会立即创建一个子类对象
- 作用: 自动创建一个子类, 并且立即创建子类对象
- 优势: 便捷的创建子类对象
- 匿名内部类实际上是有名字的, 自动生成, 格式是: 外部类名$编号.class
- 匿名内部类的对象类型, 相当于是new的那个类型子类类型
常见使用形式
通常把匿名内部类作为对象参数传递给方法使用
// 需求: 学生,老师要参加游泳比赛。
// 传统编码形式
public class Test {
public static void main(String[] args) {
// 5.调用方法,开始比赛
Swim s1 = new Teacher();
start(s1);
Swim s2 = new Student();
start(s2);
}
// 4.设计一个方法,接收老师和学生,开始比赛
public static void start(Swim s) {
sout("比赛开始...")
s.swimming();
sout("比赛结束...")
}
}
// 1.设计接口, 供子类实现
interface Swim {
void swimming(); // 游泳
}
// 2.设计老师类
class Teacher implements Swim {
@Override
public void swimming() {
sout("老师狗刨式游泳")
}
}
// 3.设计学生类
class Student implements Swim {
@Override
public void swimming() {
sout("学生蛙跳式游泳")
}
}
- 那么单独创建对象传进去就比较麻烦, 此时可以使用匿名内部类简化代码
// 需求: 学生,老师要参加游泳比赛。
// 匿名内部类形式
public class Test {
public static void main(String[] args) {
// 3.调用方法,开始比赛
Swim s1 = new Swim() {
@Override
public void swimming() {
sout("老师狗刨式游泳")
}
};
start(s1);
Swim s2 = new Swim() {
@Override
public void swimming() {
sout("学生蛙跳式游泳")
}
};
start(s2);
}
// 2.设计一个方法,接收老师和学生,开始比赛
public static void start(Swim s) {
sout("比赛开始...")
s.swimming();
sout("比赛结束...")
}
}
// 1.设计接口, 供子类实现
interface Swim {
void swimming(); // 游泳
}
- 匿名内部类都是在需要的时候用, 比如一个方法需要一个对象作为参数
- 使用匿名内部类快速的创建子类对象, 可以极大的简化代码
应用场景
调用别人提供的方法时, 这个方法需要对象作为参数, 就可以使用匿名内部类, 快速创建对象给其使用。
public class Test {
public static void main(String[] args) {
// 需求: 创建一个登录窗口,窗口上有一个登录按钮
JFrame win = new JFrame("登录窗口");
win.setSize(300, 200);
win.setLocationRelativeTo(null); // 居中展示
win.setDefaultCloseOperaion(JFrame.EXIT_ON_CLOSE);
JPanel penel new JPanel();
win.add(penel);
JButton btn = new JButton("登录");
// java要求必须给按钮添加点击事件监听器对象,这样就可以监听用户的点击操作。
// 1.传统写法: 自己定义对象, 实现监听器接口
btn.addActionListener(new ClickListener);
// 2.匿名内部类: 快速创建对象, 传给方法
btn.addActionListener(new ActionListener{
public void actionPerfirmed(ActionEvent e) {
sout("按钮点击了")
}
});
win.setVisible(true);
}
}
class ClickListener implements ActionListener {
public void actionPerfirmed(ActionEvent e) {
sout("按钮点击了")
}
}
使用comparator接口的匿名内部类实现对数组进行排序
public class Student {
private String name;
private int age;
private double height;
private String sex;
}
public class Test {
public static void main(String[] args) {
// 需求: 完成数组排序, 加深匿名内部类的使用
// 1. 准备数组,存放学生对象
student[] students = new student[4];
students[0] = new Student( name:"殷素素”,age:35,height:171.5,sex:'女');
students[1] = new Student( name:"杨幂",age:28,height: 168.5,sex:'女');
students[2] = new student( name:"张无忌"age:25,height:181.5,sex:'男');
students[3] = new student( name:"刘亦菲",age: 36,height: 168,sex:'女');
// 2.使用数组的API, 对数组按照对象的年龄升序排序
// sort()语法说明:
// public static void sort(T[] a, Comparator<T> c)
// 参数一:需要排序的数组
// 参数二:需要给sort声明一个Comparator比较器对象(指定排序的规则)
// sort()语法执行:
// sort方法会调用匿名内部类对象的compare方法,对数组中的学生对象进行两两比较,从而实现排序。
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 指定排厅规则:
// 1 如果你认为左边对象 大于 右边对象 那么返回正整数,
// 2.如果你认为左边对象 小于 石边对象 那么返回负整数。
// 3.如果两边相等那么返回0
// if(o1.getAge() > o2.getAge()) {
// return 1;
// } else if(o1.getAge() < o2.getAge()) {
// return -1;
// }
// return 0;
// 按对象年龄升序排序
return o1.getAge() - o2.getAge();
// 按对象年龄降序排序
return o2.getAge() - o1.getAge();
}
});
// 3.遍历数组中的对象并输出
for (int i = 0; i<students.length; i++) {
student s = students[i];
sout(s);
}
}
}
泛型
认识泛型
泛型提供了在编译阶段的类型约束, 并自动进行检查, 可以避免强制类型转换和可能出现的转换异常
// 该集合只能添加字符串类型的数据
ArrayList<String> list1 = new ArrayList<>();
list1.add("java1");
- 定义类,接口,方法时, 同时声明一个或多个类型变量,用于限制成员类型,
- 使用泛型的类称为泛型类, 泛型接口或泛型方法, 统称为泛型
- 原理: 把具体的数据类型作为参数传给类型变量, 通过类型变量限制成员的类型
自定义泛型类
// 模拟ArrayList集合
public class MyArrayList<E> {
private ArrayList list = new ArrayList();
public boolean add(E e) {
list.add(e);
return true;
}
public E remove(E e) {
return list.remove(e);
}
@Override
public String toString() {
return list.toString();
}
}
// 使用泛型
MyArrayList<String> mlist = new MyArrayList<>();
mlist.add("java");
mlist.add("PHP");
mlist.add("UI");
mlist.remove("java"); // true
sout(mlist); // ["PHP","UI"]
- 如果需要, 可以进一步使用extends限制泛型的范围
public class MyArrayList<E extends Animal> { }
- 表示类型E必须是Animal类型或者是Animal的子类
- 类型变量建议用大写的英文字母,常用的有:E、T、K、V等
自定义泛型接口
// 学生类
public class Student {
}
// 老师类
public class Teacher {
}
// 自定义泛型接口
// 数据操作接口
public interface Data<T> {
void add(T t);
void delete(T t);
void update(T t);
T query(int id);
}
// 学生操作实现类
public class StudentData implements Data<Student> {
@Override
public void add(Student student) {
}
@Override
public void delete(Student student) {
}
@Override
public void update(Student student) {
}
@Override
public Student query(int id) {
retuen null;
}
}
// 老师操作实现类
public class TeacherData implements Data<Teacher> {
@Override
public void add(Teacher teacher) {
}
@Override
public void delete(Teacher teacher) {
}
@Override
public void update(Teacher teacher) {
}
@Override
public Teacher query(int id) {
retuen null;
}
}
public class dome {
public static void main(String[] args) {
// 需求: 项目需要对学生/老师数据进行增删改查操作
TeacherData teacherData = new TeacherData();
teacherData.add(new Teacher());
Teacher t = teacherData.query(1);
StudentData studentData = new StudentData();
studentData.add(new Student());
Student s = studentData.query(1);
}
}
- 类型变量建议使用大写英文字母, E T K V 等
自定义泛型方法
定义方法时声明泛型变量, 才是泛型方法, 使用泛型的方法不属于泛型方法
public static void main(String[] aegs) {
String[] names = {"赵敏","张无忌","周芷若"};
Student[] stus = new Students[3];
String max = getMax(names);
Student max2 = getMax(stus);
}
public static <T> T getMax(T[] names) {
return null;
}
通配符
在使用泛型的时候可以用 ? 表示一切类型
// 汽车类
public class Car {}
// 小米汽车
public class Mi extends Car {}
// 理想汽车
public class Li extends Car {}
public static void main(String[] args) {
ArrayList<Mi> mis = new ArrayList<>();
mis.add(new Mi());
mis.add(new Mi());
mis.add(new Mi());
go(mis);
ArrayList<Li> lis = new ArrayList<>();
lis.add(new Li());
lis.add(new Li());
lis.add(new Li());
go(lis);
}
// 需求: 让汽车赛跑
// 报错: 虽然Mi和Li都是Car的子类, 但是Arraylist<Mi> ArrayList<Li>和 Arraylist<Car> 是没有关系的
public static void go(ArrayList<Car> cars) { }
// 使用通配符, 表示一切类型, 这个方法Mi汽车和Li汽车都能用
public static void go(ArrayList<?> cars) { }
泛型上下限
// 狗
public class Dog {}
public static void main(String[] args) {
ArrayList<Li> lis = new ArrayList<>();
lis.add(new Li());
lis.add(new Li());
lis.add(new Li());
go(lis);
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
go(dogs);
}
// 需求: 让汽车赛跑
// 缺点: 狗也跑进来了, 约束性太弱了
public static void go(ArrayList<?> cars) { }
// 使用泛型上限: 能接受的类型必须是Car或其子类
// 狗类不是汽车类或其子类, 就进不来了
public static void go(ArrayList<? extedns Car> cars) { }
- 泛型上限: ? extends Cart
- 表示能接受的类型必须是Car或其子类
- 泛型下限: ? super Cart
- 表示能接受的必须是Cart或其父类
泛型擦除
泛型不支持基本数据类型, 只支持对象类型, 如Integer Double
public static void main(String[] ages) {
// 报错: 泛型不支持基本数据类型
ArrayList<int> list = new ArrayList<>()
// 原因: 泛型擦除导致的
// 1.泛型是在编译阶段工作的. 就是帮助程序进行类型校验的
// 2.程序编译成class文件后, 泛型变量全部恢复为Object类型
// 3.假设往集合中添加了基本类型的数据
list.add(12);
// 4.字节码文件中就会使用Object接收数据, 基本类型的数据不能赋值给对象类型, 造成报错
Object e = 12; // 所以, 泛型不支持基本类型数据的
}
包装类
包装类就是8种基本类型数据对应的引用类型
- 从理念上说., Java为了实现一切皆对象, 为8种基本类型提供了对应的引用类型
- 从技术上说, 泛型和集合只支持包装类型, 所以需要包装类
相互转换
// 手动包装: 把基本类型数据转为引用类型
Integer a = Integer.valueOf(12); // 得到对象12
// 缓存优化: -128到127之间的数据经常用, java就封装了共享对象
Integer a1 = Integer.valueOf(100);
Integer a2 = Integer.valueOf(100);
sout(a1 == a2); // true, 因为指向同一个对象
Integer a1 = Integer.valueOf(130);
Integer a2 = Integer.valueOf(130);
sout(a1 == a2); // false, 超过了边界,创建新对象
//自动装箱: 基本类型的数据可以直接赋值给包装类型的变量
Integer i = 130;
//自动拆箱: 包装类型的数据可以直接赋值给基本类型的变量
int ii = i;
//自动装箱和拆箱的优势: 完全自动,不增加我们的负担
ArrayList<Integer> list = new ArratList<>();
list.add(120); // 自动装箱
list.add(110); // 自动装箱
int res1 = list.get(0); // 自动拆箱
int res2 = list.get(1); // 自动拆箱
包装类提供的方法
1.把基本类型的数据转成字符串
Integer a = 23;
String s1 = Integer.toString(a); // "23"
String s2 = a.toString(); // "23"
String s3 = a + ""; // "23"
2.把字符串类型的数值转成真实的类型数据
String ageStr = "29";
int age = Integer.parseInt(ageStr); //29
int age = Integer.valueOf(ageStr); // 29(推荐)(容易记)
String scoreStr = "99.5";
double score = Double.parseDouble(scoreStr); // 99.5
double score = Double.valueOf(scoreStr); // 99.5(推荐)(容易记)
// 注意
// 要转换的数据一定要和类型对应
String str1 = "29.5";
int age = Integer.parseInt(str1); //报错, 29.5不能转成29
String str2 = "29a";
int age = Integer.parseInt(str2); //报错, 29a不能转成整数
软件包
包就是分门别类的管理类的, 类似于文件夹
建包
// 语句
// package 公司域名倒写.技术名称
// 必须在第一行, 一般IEDA工具会帮助创建
package com.itheima.javabean;
public class Student {
}
导包
相同包下的类可以直接访问
- 不同包下的类必须导包, 语句: import 包名.类名;
- 使用java官方的程序, 也需要导包, lang包下的程序可以直接用
- 一个类中使用多个不同包下的类, 如果类的名字一样, 默认只能导入一个, 另一个必须带包名访问
// 访问多个其他包下的程序,这些程序名又一样的情况下,默认只能导入一个程序,
// 另一个程序必须带包名和类名来访问。
Demo2 d2 = new Demo2();
d2.myPrint();
com.itheima.pkg.itcast.Demo2 d3 = new com.itheima.pkg.itcast.Demo2();
d3.myPrint();
自动导包