个人主页-爱因斯晨
文章专栏-JAVA专栏
本文是对面向对象的超详细讲解,包含并融合前面两篇文章。耗时长、篇幅多达2w字,希望给大家带来帮助与启发。 新人博主,文章中如有表述不清或错误之处,还请各位大佬批评指正!如本文对您帮助很大,可以私信博主获取PDF版本。承蒙厚爱,感谢支持!
文章目录
个人主页-爱因斯晨**[文章专栏-JAVA专栏](https://blog.csdn.net/2401_87533975/category_12883400.html?spm=1001.2014.3001.5482)**
一、设计对象并使用1.类和对象2.类的几个补充注意事项3.开发中类的设计
二、封装1.封装的介绍2.封装的好处3.private关键字
三、this关键字1.成员变量和局部变量2.举例代码详细解释:1. 类的成员变量定义2. 构造方法中的 `this` 关键字使用3. `set` 方法中的 `this` 关键字使用4. `get` 方法中的 `this` 关键字使用5. `displayInfo` 方法中的 `this` 关键字使用
四、构造方法1.构造方法的概述2.构造方法的格式3.构造方法的作用4.构造方法的分类5.构造方法的注意事项
五、标准JavaBean1.标准的JavaBean类
六、对象内存图1.一个对象的内存图内存执行顺序解析(基于Java内存模型)**1. 类加载阶段(方法区)****2. 栈内存操作(main方法启动)****3. 堆内存分配(对象实例化)****4. 对象初始化流程****5. 变量关联与操作****6. 方法调用(方法区与栈协作)**
**内存操作完整流程总结****关键现象解释**
2.多个对象的内存图**2.1、执行顺序与内存操作分步解析****1. 加载class文件(方法区)****2. 声明局部变量(栈内存)****3. 堆内存分配(对象空间开辟)****4. 默认初始化(堆内存)****5. 显示初始化(堆内存)****6. 构造方法初始化(堆内存)****7. 地址赋值(栈内存)**
**2.2、内存模型与对象独立性的关键验证****1. 对象独立性的体现****2. 内存操作流程图解**
**2.3、执行流程总结(分阶段)****2.4、常见问题解答****1. 为什么`System.out.println(s)` 输出地址?****2. 显示初始化和构造方法初始化有何区别?****3. 如何优化内存使用?**
3.两个变量指向同一个对象内存图3.1、类加载阶段(方法区)3.2、栈内存操作(main方法栈帧)3.3、堆内存操作(对象关联)3.4、最终内存结构3.5、输出结果分析
4.this的内存原理4.1、类加载阶段(方法区核心操作)4.2、对象实例化流程(堆栈协同)4.3、方法调用时的内存隔离(栈帧作用域)4.4、关键差异对比表4.5、技术扩展:`this`的底层实现
5.基本数据类型和引用数据类型的区别5.1基本数据类型5.2引用数据类型
七、补充知识:成员变量、局部变量区别八、static(静态)1.static 静态变量代码展示内存图
2.static 静态方法工具类:练习:
3.static注意事项4.重新认识`main`方法
九、继承1.继承概述2.继承的特点3.子类到底能继承父类中的哪些内容4.继承中访问特点继承中:成员变量的访问特点继承中:成员方法的访问特点继承中:构造方法的访问特点this、super使用总结
十、多态1.引言2.多态2.1 概览多态什么是多态:多态的表现形式:多态的前提:多态的好处
2.2 多态中调用成员的特点2.3 多态的优势
十一、面向对象细节性知识点1.包什么是包?包的命名规则使用其他类的规则
2.final关键字**方法:****类:****变量:**补充:常量细节:
3.权限修饰符权限修饰符:权限修饰符的分类:权限修饰符的使用规则
4.代码块
十二、抽象类、接口、内部类1.抽象类引言:抽象方法抽象类抽象类和抽象方法的定义格式抽象类和抽象方法的注意事项抽象类与抽象方法的意义
2.接口接口的定义与使用接口中成员方法的特点接口和类的关系JDK8开始接口中新增的方法适配器设计模式
3.内部类内部类内部类的访问特点:内部类的分类成员内部类静态内部类局部内部类匿名内部类(重点掌握)局部内部类匿名内部类(重点掌握)
面向:拿、找
对象:能干活的东西
面向对象编程:拿东西过来做对应的事情
面向对象编程的例子:
import java.util.Random;
import java.util.Scanner;
public class mian {
public static void main(String[] args) {
//面向对象,导入一个随机数
Random r = new Random();
int data = r.nextInt(10)+1;
//面向对象,输入一个随机数
System.out.println(data);
Scanner sc = new Scanner(System.in);
// 面向对象,输出一个数
System.out.println("请输入一个数:");
int a = sc.nextInt();
System.out.println(a);
}
}
为什么java要采取这种方法来编程呢?
我们在程序之中要干某种事,需要某种工具来完成,这样更符合人类的思维习惯,编程更简单,更好理解。
面向对象的重点学习对象是什么? 学习获取已有对象并使用,学习如何自己设计对象并使用。——面向对象的语法
一、设计对象并使用
1.类和对象
类(设计图):是对象共同特征的描述
如何定义类:
public class 类名{
1.成员变量(代表属性,一般是名词)
2.成员方法(代表行为,一般是动词)
3.构造器(后面学习)
4.代码块(后面学习)
5.内部类(后面学习)
}
public class Phone{
//属性(成员变量)
String brand;
double price;
public void call(){
}
public void playGame(){
}
}
如何得到对象?
如何得到类的对象:
类名 对象名= new 类名();
Phone p = new Phone();
对象:是真实存在的具体东西
拿到对象后能做什么?
对象.成员变量;
对象.成员方法(...)
在JAVA中,必须先设计类,才获得对象
public class phone {
//属性
String name;
double price;
public void call(){
System.out.println("打电话");
}
public void send(){
System.out.println("发短信");
}
}
//测试
public class phoneTest {
public static void main(String[] args) {
//创建手机对象
phone p = new phone();
//给手机对象赋值
p.name = "小米";
p.price = 1999;
//获取手机对象的属性值
System.out.println(p.name);
System.out.println(p.price);
//调用手机对象的方法
p.call();
p.send();
}
}
2.类的几个补充注意事项
用来描述一类事物的类,专业叫做:Javabean类。
在javabean类中,是不写main方法的。
在以前,编写main方法的类,叫做测试类。
我们可以在测试中创建javabean类的对象并进行赋值调用。
public class 类名 {
1.成员变量(代表属性)
2.成员方法(代表行为)
}
public class Student {
//属性(成员变量)
String name;
int age;
//行为方法
public void study(){
System.out.println("好好学习,天天向上");
}
public void doHomework(){
System.out.println("键盘敲烂,月薪过万");
}
}
类名首字母建议大写,需要见名知意,驼峰模式。
一个java文件中可以定义多个class类,且只能一个类是public修饰的类名必须成为代码文件名。
实际开发中建议还是一个文件定义一个class类。
成员变量的完整定义格式是:修饰符 数据类型 变量名称=初始化值;一般无需指定初始化值,存在默认值。
int age;
//这里不写初始化值是因为,这里学生的年龄是一个群体的值,没有一个固定的初始化值。
//如果给age赋值,比如是18岁,那就代表者所有的学生年龄都是18岁。
//类的赋值不是在类里面赋值,而是在创建了对象之后再赋值,这时赋值的时这个特定的对象。
Student stu = new Student();
Stu.name="张三";
Stu.height=187;
对象的成员变量的默认值规则
数据类型明细默认值基本类型byte,short,int,long0基本类型float,double0.0基本类型booleanfalse引用类型类、接口、数组、Stringnull//编写女朋友类,创建女朋友类的对象,给女朋友的属性赋值并调用女朋友类中的方法。自己思考女朋友有哪些属性,有哪些行为?
public class girlFriend {
public static void main(String[] args) {
//创建女朋友对象
girl g = new girl();
//给女朋友对象赋值
g.name = "小红";
g.age = 20;
g.hobby = "唱歌";
//获取女朋友对象的属性值
System.out.println(g.name);
System.out.println(g.age);
System.out.println(g.hobby);
//调用女朋友对象的方法
g.eat();
g.sleep();
}
}
//这是一个类
public class girl {
//成员变量(代表属性)
String name;
int age;
String hobby;
//成员方法(代表行为)
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
3.开发中类的设计
先把需求拿过来,先要看这个需求当中有几类事物。每个事物,每类事务都要定义为单独的类,这类事物的名词都可以定义为属性,这类事物的功能,一般是动词,可以定义为行为。
二、封装
1.封装的介绍
封装是面向对象的三大特征:封装、继承、多态
封装的作用:告诉我们,如何正确设计对象的属性和方法。
/**需求:定义一个类描述人
属性:姓名、年龄
行为:吃饭、睡觉*/
public class Person{
String name;
int age;
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为。
public class Circle {
double radius;
public void draw(){
System.out.println("根据半径"+radius+"画圆");
}
}
//人画圆,我们通常人为行为主体是人,其实是圆
//例如:人关门,这个门一定是门自己关的,人只是给了作用力,是门自己关上的。
2.封装的好处
对象代表什么,就得封装对应的数据,并提供数据对应的行为降低我们的学习成本,可以少学,少记,或者说压根不用学,不用记对象有哪些方法,有需要时去找就行
3.private关键字
是一个权限修饰符
可以修饰成员(成员变量和成员方法)
被private修饰的成员只能在本类中才能访问
public class GirlFriend{
private String name;
private int age;
private String gender;
}
public class leiMing {
private int age;
//set(赋值)
public void setAge(int a){
if(a<0||a>120){
System.out.println("你给的年龄有误");
return;
}
age = a;
}
//get(取值)
public int getAge(){
return age;
}
}
针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作
提供“setXxx(参数)”方法,用于给成员变量复制,方法用public修饰
提供“getXxx()”方法,用于获取成员变量的值,方法用public修饰
为什么要调用set和get呢?
封装是面向对象编程的四大特性之一,它将数据(成员变量)和操作数据的方法绑定在一起,并隐藏对象的内部实现细节。通过将成员变量声明为 private,外部类无法直接访问和修改这些变量,只能通过类提供的 set 和 get 方法来间接操作。这样可以防止外部代码对数据进行非法或不恰当的修改,保证数据的安全性和完整性。
三、this关键字
1.成员变量和局部变量
public class GirlFriend{
private int age;//成员变量:方法的外面,类的里面
public void method(){
int age = 10;//局部变量:方法的里面
System.out.println(age);
}
}
成员变量和局部变量一致时,采用就近原则
谁离我近,我就用谁
public class GirlFriend{
private int age;//成员变量:方法的外面,类的里面
public void method(){
int age = 10;//局部变量:方法的里面
System.out.println(age);
}
}
//在这里中,最后1个age距离 age=10最近,所以最后一个age用的是10的值
//假如我想用第一个int ,我们可以在System.out.println(this.age)
age前加入:this. 这里就可以打破就近原则,选择另一个变量
在 Java 中,当局部变量(比如方法的参数)和类的成员变量重名时,就会产生命名冲突。在这种情况下,如果直接使用变量名,Java 默认会使用局部变量。而 this 关键字的一个重要作用就是用来引用当前对象的成员变量,从而区分局部变量和成员变量。
2.举例
下面通过一个简单的示例来详细讲解从引用成员变量方向 this 关键字的用法:
class Employee {
// 定义成员变量
private String name;
private int age;
// 构造方法,用于初始化员工信息
public Employee(String name, int age) {
// 这里参数名和成员变量名相同,使用 this 引用成员变量
this.name = name;
this.age = age;
}
// 设置员工姓名的方法
public void setName(String name) {
// 使用 this 引用成员变量
this.name = name;
}
// 获取员工姓名的方法
public String getName() {
return this.name;
}
// 设置员工年龄的方法
public void setAge(int age) {
// 使用 this 引用成员变量
this.age = age;
}
// 获取员工年龄的方法
public int getAge() {
return this.age;
}
// 显示员工信息的方法
public void displayInfo() {
System.out.println("姓名: " + this.name + ", 年龄: " + this.age);
}
}
public class ThisKeywordVariableExample {
public static void main(String[] args) {
// 创建一个 Employee 对象
Employee employee = new Employee("李四", 25);
// 调用 displayInfo 方法显示员工信息
employee.displayInfo();
// 调用 setName 和 setAge 方法修改员工信息
employee.setName("王五");
employee.setAge(30);
// 再次调用 displayInfo 方法显示修改后的员工信息
employee.displayInfo();
}
}
代码详细解释:
1. 类的成员变量定义
private String name;
private int age;
这里定义了两个私有成员变量 name 和 age,用于存储员工的姓名和年龄。
2. 构造方法中的 this 关键字使用
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
在构造方法中,参数名 name 和 age 与类的成员变量名相同。此时,this.name 表示当前对象的成员变量 name,而直接使用的 name 则是构造方法的参数(局部变量)。通过 this.name = name; 语句,将局部变量 name 的值赋给了当前对象的成员变量 name。同理,this.age = age; 也是将局部变量 age 的值赋给了成员变量 age。
3. set 方法中的 this 关键字使用
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
在 setName 和 setAge 方法中,同样存在参数名和成员变量名相同的情况。使用 this 关键字来明确指定要操作的是当前对象的成员变量,避免了与局部变量的混淆。
4. get 方法中的 this 关键字使用
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
在 get 方法中,使用 this.name 和 this.age 来返回当前对象的成员变量的值。虽然在这种情况下,不使用 this 关键字也可以正常返回成员变量的值,因为这里没有局部变量与成员变量重名的问题,但使用 this 可以使代码的意图更加清晰,表明是在访问当前对象的成员变量。
5. displayInfo 方法中的 this 关键字使用
public void displayInfo() {
System.out.println("姓名: " + this.name + ", 年龄: " + this.age);
}
在 displayInfo 方法中,使用 this.name 和 this.age 来获取当前对象的成员变量的值,并将其输出。
四、构造方法
1.构造方法的概述
构造方法也叫做构造器、构造函数
2.构造方法的格式
public class Student{
修饰符 类名(参数){
方法体;
}
}
public class Student {
private String name;
private int age;
//如果我们自己没有写构造方法
// 那么编译器会自动生成一个无参构造方法
public Student() {
System.out.println("无参构造方法");
}
public Student(String name, int age) {
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;
}
}
public class StudentTest {
public static void main(String[] args) {
//创建类的对象
//调用的空参构造
//Student s1 = new Student();
Student s = new Student(name:"张三", age:20);
System.out.println(s.getName());
System.out.println(s.getAge());
}
}
特点:
方法名与类名相同,大小写也要一致没有返回值类型,连void都没有没有具体的返回值(不能由return带回结果数据)
执行时机:
创建对象的时候由虚拟机调用,不能手动调用构造方法每创建一次对象,就会调用过一次构造方法
3.构造方法的作用
在创建对象的时候,由虚拟机自动调用构造方法,作用是给成员变量进行初始化的
4.构造方法的分类
public class Student{
private String name;
private int age;
public Student(){
...//空参构造方法
}
public Student (String name, int age){
....//带全部参数构造方法
}
}
无参构造方法:初始化的对象时,成员变量的数据均采用默认值
有参构造方法:在初始化对象的时候,同时可以为对象进行
5.构造方法的注意事项
构造方法的定义
如果没有定义构造方法,系统将给出一个默认的无参数构造方法如果定义了构造方法,系统将不再提供默认的构造方法 构造方法的重载
带参构造方法,和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载 推荐的使用方式
无论是否使用,都动手书写无参数构造方法,和带全部参数的构造方法
五、标准JavaBean
1.标准的JavaBean类
类名需要见名知意成员变量使用private修饰提供至少两个构造方法
无参构造方法带全部参数的构造方法 成员方法
提供每一个成员变量对应的setXxx()/getXxx()如果还有其他行为,也需要写上
举例子:
根据一个登录界面写一个JavaBean类
public class User {
//属性
private String username;
private String password;
private String email;
private String gender;
private int age;
//构造方法
//无参构造
public User() {
}
//有参构造
public User(String username, String password, String email, String gender, int age) {
this.username = username;
this.password = password;
this.email = email;
this.gender = gender;
this.age = age;
}
//方法
//set和get方法
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
我们再写一个javabean中会遇到一个问题:
这样写纯体力活啊!没事的没事的!我们有快捷键:
方法一:
alt+insert 或 alt+insert+Fn
alt+insert 第一个是构造函数,点击无选择生成的是空参 ,全选ok生成的是有参数的构造函数
alt+insert 点击setter和geteer,全选生成的是set和get
方法二:
下载插件pdg,下载完成后点击空白处就会出现。然后点击Ptg To JavaBean
六、对象内存图
1.一个对象的内存图
Student s = new Student();
加载class文件申明局部变量在堆中开辟一个空间默认初始化显示初始化构造方法初始化将堆中空间的地址值赋值给左边的局部变量
举例:
public class Student{
String name;
int age;
public void study(){
System.out.println("好好学习")
}
}
public class TestStudent{
public static void main(String [] args){
Student s= new Student();
System.out.println(s);
System.out.println(s.name+"...."+s.age);
s.name = "阿强";
s.age = 23;
System.out.println(s.name+"..."+s.age);
s.study();
}
}
解析:
内存执行顺序解析(基于Java内存模型)
1. 类加载阶段(方法区)
加载class文件
JVM将
Student.class
和
TestStudent.class
加载到方法区,存储类结构信息(字段、方法签名、常量池等)。
Student类包含字段name(String)、age(int)和方法study()。TestStudent类包含main()方法入口。
2. 栈内存操作(main方法启动)
声明局部变量 执行main()时,在栈内存中创建main方法的栈帧,声明局部变量s(此时s未指向任何对象,值为null)。
3. 堆内存分配(对象实例化)
在堆中开辟空间 执行new Student()时,在堆内存中为Student对象分配空间,内存大小由字段类型决定(String引用 + int值)。
4. 对象初始化流程
默认初始化 对象字段赋默认值:
name → null(引用类型默认值)age → 0(基本类型默认值)。 显示初始化(本例中无) 如果类中字段有显式赋值(如String name = "默认";),此时会执行。但示例代码未定义,此步骤跳过。构造方法初始化(本例中无) 如果存在构造方法(如public Student() { age = 18; }),会通过构造器赋值。示例代码未定义构造方法,此步骤跳过。
5. 变量关联与操作
地址赋值给局部变量
堆中对象地址赋值给栈帧中的
s
变量,完成引用关联。
执行Student s = new Student();后,s指向堆内存中的对象。 对象字段修改 后续代码通过s.name = "阿强";和s.age = 23;直接修改堆中对象的字段值,无需重新初始化。
6. 方法调用(方法区与栈协作)
执行s.study()
从方法区加载study()的字节码指令。在栈中创建study()方法的栈帧,执行System.out.println(" 好好学习")(注:用户代码此处缺少分号,实际会编译报错)。
内存操作完整流程总结
步骤操作内容内存区域示例代码体现1加载类信息方法区Student和TestStudent类加载2声明局部变量s栈内存Student s;3堆中分配对象空间堆内存new Student()4字段默认初始化(null/0)堆内存s.name 和s.age 初始值5显式/构造初始化(无)-代码未定义相关逻辑6对象地址赋值给s栈内存s = new Student();7修改字段值堆内存s.name = "阿强";等操作
关键现象解释
System.out.println(s) 输出哈希值 因打印对象时默认调用toString(),而Student未重写该方法,输出格式为类名@哈希值。字段值修改的可见性 直接通过引用s修改堆中对象字段,所有指向该对象的引用均会看到更新后的值。编译隐患 study()方法中System.out.println(" 好好学习")缺少分号,实际运行前会因语法错误中断。
2.多个对象的内存图
举例:
public class Student{
String name;
int age;
public void study(){
System.out.println("好好学习");
}
}
public class TestStudent{
public static void main(String [] args){
Student s= new Student();
System.out.println(s);
s.name = "阿强";
s.age = 23;
System.out.println(s.name+"..."+s.age);
s.study();
Student s2= new Student();
System.out.println(s2);
s2.name = "阿珍";
s2.age = 24;
System.out.println(s2.name+"..."+s2.age);
s2.study();
}
}
第二次在创建对象时。class文件不需要再加载一次
解析:
2.1、执行顺序与内存操作分步解析
1. 加载class文件(方法区)
触发条件:首次使用Student类时。操作内容:
将Student.class 加载到方法区,存储类结构(字段name、age和方法study()的定义)。将TestStudent.class 加载到方法区,存储main()方法入口。
2. 声明局部变量(栈内存)
操作内容:
执行main()方法时,在栈内存创建main方法的栈帧。声明局部变量s和s2(初始值均为null)。
3. 堆内存分配(对象空间开辟)
操作内容:
new Student()触发堆内存分配,根据类结构计算对象大小(String引用 + int值)。示例:
s = new Student() → 堆地址0x001。s2 = new Student() → 堆地址0x002(独立空间)。
4. 默认初始化(堆内存)
操作内容:
对象字段赋默认值:
name → null(引用类型默认值)。age → 0(基本类型默认值)。 示例:
s的初始状态:name=null, age=0。s2的初始状态:name=null, age=0。
5. 显示初始化(堆内存)
触发条件:类中显式赋值的字段(如String name = "默认")。当前代码:
Student类未定义显式赋值字段,跳过此步骤。
6. 构造方法初始化(堆内存)
触发条件:存在自定义构造方法(如public Student() { ... })。当前代码:
Student类未定义构造方法,使用默认无参构造器,跳过此步骤。
7. 地址赋值(栈内存)
操作内容:
将堆内存地址赋值给栈中的局部变量。示例:
s = 0x001(指向第一个对象)。s2 = 0x002(指向第二个对象)。
2.2、内存模型与对象独立性的关键验证
1. 对象独立性的体现
对象堆地址字段修改后的值s0x001name="阿强", age=23s20x002name="阿珍", age=24
验证逻辑:
s和s2指向不同堆地址,修改其中一个对象的字段不会影响另一个对象。System.out.println(s == s2) → 输出false。
2. 内存操作流程图解
2.3、执行流程总结(分阶段)
阶段操作内容内存区域类加载加载Student和TestStudent类信息方法区栈帧创建声明s和s2(初始null)栈内存堆内存分配为s和s2分配独立空间堆内存对象初始化默认初始化 → 显式赋值(用户代码修改)堆内存方法调用study()从方法区加载逻辑到栈执行栈内存
2.4、常见问题解答
1. 为什么System.out.println(s) 输出地址?
原因:未重写toString()方法,默认调用Object.toString() ,格式为类名@哈希值。
2. 显示初始化和构造方法初始化有何区别?
显示初始化:直接在类中赋值字段(如String name = "张三"),编译时自动插入到构造方法中。构造方法初始化:通过自定义构造器赋值(优先级高于显示初始化)。
3. 如何优化内存使用?
复用对象:避免频繁new对象(尤其循环中)。垃圾回收:main()结束后,s和s2成为垃圾对象,由GC自动回收。
附:修正后的代码输出示例
Student@1b6d3586
阿强...23
好好学习
Student@4554617c
阿珍...24
好好学习
3.两个变量指向同一个对象内存图
举例:
public class Student{
String name;
int age;
public void study(){
System.out.println("好好学习");
}
}
public class TestStudent{
public static void main(String [] args){
Student s= new Student();
s.name = "阿强";
Student s2= s;
s2.name = "阿珍";
System.out.println(s.name+"..."+s2.name);
}
}
3.1、类加载阶段(方法区)
加载TestStudent.class 当JVM启动时,首先将TestStudent.class 加载到方法区,存储类结构信息(成员方法、字段描述等)加载Student.class 执行new Student()时触发类加载机制,将Student.class 加载到方法区,包含name、age字段和study()方法元数据
3.2、栈内存操作(main方法栈帧)
声明局部变量 在main方法栈帧中创建引用变量s(地址未初始化)和s2(此时两者均为null)对象创建指令 new Student()操作码触发堆内存分配,此时:
在堆中生成对象内存空间(包含对象头 + String name + int age)默认初始化:name=null,age=0(基本类型和引用类型的零值初始化)显式初始化:由于Student类没有直接赋值的字段(如String name = "默认名"),此阶段跳过 构造方法执行 若存在构造方法(本案例未定义),会通过invokespecial指令调用
3.3、堆内存操作(对象关联)
地址赋值 将堆中Student对象地址赋值给栈帧中的s变量(完成s = new Student())引用传递 s2 = s操作使s2指向堆中同一个对象(此时两个引用共享对象数据)字段修改 通过s2.name = "阿珍"修改堆内存对象数据,此时s.name 同步变化(引用指向同一实体)
3.4、最终内存结构
内存区域存储内容方法区TestStudent类字节码、Student类元数据(包含study()方法代码)堆内存Student对象实例(name=“阿珍”, age=0)栈内存main方法栈帧:s=0x100(指向堆对象), s2=0x100(与s同地址)
3.5、输出结果分析
System.out.println(s.name+"..."+s2.name) → 输出阿珍...阿珍(s与s2引用同一对象,堆内数据修改对所有引用可见)
关键理解点:引用类型变量的赋值操作传递的是对象地址值,而非创建新对象。这种特性是Java对象共享机制的核心体现。
4.this的内存原理
public class Student{
private int age;
public void method(){
int age=10;
System.out.println(age);//10
System.out.println(this.age);//成员变量的值 0
}
}
this的作用:区分局部变量和成员变量
this的本质:所在方法调用者的地址值
public class Student{
private int age;
public void method(){
int age=10;
System.out.println(age);//10
System.out.println(this.age);//成员变量的值 0
}
}
public class StudentTest{
public static void main (String[] args){
Student s = new Student();
s.method();
}
}
4.1、类加载阶段(方法区核心操作)
加载StudentTest.class
JVM启动时优先加载含main()的类到方法区存储类元数据:静态变量、方法表(含main()入口地址) 触发Student.class 加载
当执行new Student()时触发类加载方法区新增:
字段描述表(private int age的访问权限和偏移量)method()的字节码指令集合隐式默认构造器
4.2、对象实例化流程(堆栈协同)
步骤内存区域具体行为代码对应3栈内存在main方法栈帧声明局部变量s(初始值null)Student s;4堆内存分配对象空间:对象头(12字节)+ int age(4字节)= 16字节new Student()5堆内存默认初始化:age=0(基本类型零值填充)隐式执行6堆内存构造方法初始化:执行空参数的
4.3、方法调用时的内存隔离(栈帧作用域)
执行s.method() 时发生:
新建栈帧:在栈顶创建
method()
的独立空间,包含:
隐式参数this(指向堆地址0x7a3f)局部变量age=10(存储于栈帧变量表) 变量访问规则:
输出语句内存访问路径结果System.out.println(age)访问栈帧局部变量表10System.out.println(this.age)通过this指针访问堆内存字段0
4.4、关键差异对比表
特征成员变量this.age局部变量age存储位置堆内存对象内部栈帧局部变量表生命周期与对象共存亡随方法栈帧销毁而消失初始化值默认零值(int=0)必须显式赋值访问方式需通过对象引用直接访问
4.5、技术扩展:this的底层实现
当调用method()时:
字节码层面:
java复制aload_0 // 将this引用压入操作数栈(对应堆地址0x7a3f)
getfield #2 // 根据字段偏移量读取堆中age值(#2为字段符号引用)
内存隔离机制:局部变量age会遮蔽同名的成员变量,必须通过this.显式穿透访问堆数据
5.基本数据类型和引用数据类型的区别
5.1基本数据类型
public class Test{
public static void main (String [] args){
int a = 10;
}
}
基本数据类型:
在变量当中存储的是真实的数据值
从内存角度:数据值是存储再自己的空间中
特点:赋值给其他变量,也是赋值的真实的值。
5.2引用数据类型
public class TestStudent{
public static void main(String[] args){
Student s=new Student;
}
}
引用数据类型:
堆中存储的数据类型,也就是new出来的,变量中存储的是地址值。引用:就是使用其他空间中数据的意思。
从内存的角度:
数据值是存储在其他空间中,自己空间中存储的是地址值
特点:
赋值给其他变量,赋的地址值。
七、补充知识:成员变量、局部变量区别
成员变量:类中方法外的变量
局部变量:方法中的变量
区别:
区别成员变量局部变量类中位置不同类中,方法外方法内、方法申明上初始化值不同有默认初始化值没有,使用前需要完成赋值内存位置不同堆内存栈内存生命周期不同随着对象的创建而存在,随着对象的消失而消失随着方法的调用而存在,随着方法的运行结束而消失作用域整个类中有效当前方法中有效
面向对象进阶一:静态与继承
八、static(静态)
static表示静态,是Java中的一个修饰符,可以修饰成员方法,成员变量。
1.static 静态变量
被static修饰的成员变量,叫做静态变量。
特点:
被该类的所有对象共享
不属于对象,属于类
随着类的加载而加载,优于对象存在
调用方式:
类名调用(推荐)
对象名调用
代码展示
需求:写一个JavaBean类来描述这个班级的学生
属性:姓名、年龄、性别
行为:学习
JAVA Bean 类
package staticdemo;
public class Student {
//属性:姓名,年龄,性别
//新增:老师的姓名
private String name;
private int age;
private String gender;
public static String teacherName;
public Student() {
}
public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return gender
*/
public String getGender() {
return gender;
}
/**
* 设置
* @param gender
*/
public void setGender(String gender) {
this.gender = gender;
}
//行为
public void study(){
System.out.println(name+"正在学习");
}
public void show(){
System.out.println(name+","+age+","+gender+","+teacherName);
}
}
测试类
package staticdemo;
public class StudentTest {
public static void main(String[] args) {
//1.创建第一个学生对象
Student s1 = new Student();
s1.setName("张三");
s1.setAge(20);
s1.setGender("男");
//公共类,但是s2没有创建对象,所以无法访问teacherName,为null
//public String teacherName;
//于是我们想了个方法,用static修饰teacherName,但是这样就变成了静态属性,所有对象共享
//public static String teacherName;
s1.teacherName = "王老师";
//还可以用类名.属性名来访问
//Student.teacherName = "王老师";
s1.study();
s1.show();
//2.创建第二个学生对象
Student s2 = new Student();
s2.setName("李四");
s2.setAge(21);
s2.setGender("女");
s2.study();
s2.show();
}
}
内存图
栈内存
方法调用时会在栈内存中创建栈帧。这里main方法首先入栈 ,在main方法执行过程中:
执行 Student.teacherName = "阿玮老师"; ,这一步只是对静态变量赋值,在栈内存中记录这个操作。执行 Student s1 = new Student(); 时,在栈内存为引用变量 s1 分配空间,存放指向堆内存中 Student 对象的地址(假设为 0x0011 ) 。执行 s1.name = "张三"; 和 s1.age = 23; ,是通过 s1 引用操作堆内存中对象的实例变量。执行 s1.show(); 时,show 方法的栈帧入栈,在栈帧中记录方法内的局部变量(这里无额外局部变量)以及要操作的对象属性(通过 s1 找到堆内存对象属性)。执行 Student s2 = new Student(); ,在栈内存为引用变量 s2 分配空间,存放指向堆内存中另一个 Student 对象的地址(假设为 0x0022 ) 。执行 s2.show(); 时,show 方法栈帧再次入栈,通过 s2 引用操作其对应的堆内存对象属性。 堆内存
当执行new Student()时,在堆内存创建Student对象实例。
第一个 Student 对象(对应 s1 ),在堆内存中分配空间存储实例变量 name 值为 “张三” ,age 值为 23 。第二个 Student 对象(对应 s2 ),在堆内存中分配空间存储实例变量 name 初始值 null (字符串默认初始值) ,age 初始值 0 (整数默认初始值) 。 静态变量 teacherName 存储在堆内存的静态存储位置(静态区),值为 “阿玮老师” ,所有 Student 类的对象共享这个静态变量。
注意:静态变量随类的出现而出现,优于变量。
2.static 静态方法
被static修饰的成员方法,叫做静态方法。
特点:
多用在测试类和工具类中
Javabean类中很少会用
调用方式:
类名调用(推荐)
对象名调用
工具类:
帮助我们做一些事情的,但是不描述任何事物的类
Javabean类:用来描述一类事物的类。比如:Student、Teather、Dog等
测试类:用来检查其他类是否书写正确,带有main方法的类,是程序的入口
遵守的规范:
类名见名知意私有化构造方法方法定义为静态的
练习:
第一题:
需求:在实际开发中,经常会遇到一些数组使用的工具类
请按照如下要求编写一个数组的工具类:ArrayUtil
工具类:
package sta02;
public class ArrayUtil {
//私有构造方法,防止外部实例化
private ArrayUtil() {}
public static String printArray(int[] arr) {
StringBuilder sb = new StringBuilder();
sb.append( "[");
for (int i = 0; i < arr.length; i++) {
sb.append(arr[i]);
if (i < arr.length - 1) {
sb.append(", ");
}else {
sb.append("]");
}
}
return sb.toString();
}
public static double getArray(double[] arr) {
double sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i]; //累加数组元素
}
return sum/arr.length;
}
}
测试类
package sta02;
public class Testdemo {
public static void main(String[] args) {
//测试printArray方法
int[] arr = {1, 2, 3, 4, 5};
String result = ArrayUtil.printArray(arr);
System.out.println(result);
//测试getArray方法
double[] arr2 = {1.0, 2.0, 3.0, 4.0, 5.0};
double average = ArrayUtil.getArray(arr2);
System.out.println(average);
}
}
第二题:
需求:定义一个集合,用于存储3个学生对象
学生类的属性:name、age、gender
定义一个工具类,用于获取集合中最大学生的年龄
JavaBean类:
package sat03;
public class Student {
private String name;
private int age;
private String gender;
public Student() {
}
public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return gender
*/
public String getGender() {
return gender;
}
/**
* 设置
* @param gender
*/
public void setGender(String gender) {
this.gender = gender;
}
}
方法类:
package sat03;
import java.util.ArrayList;
public class StudentUtil {
//私有构造方法,防止外部实例化
private StudentUtil() {}
//静态方法
public static int getMaxScore(ArrayList
//1.定义一个参照物
int maxAge = list.get(0).getAge();
//2.遍历集合
for (int i = 1; i < list.size(); i++) {
if (list.get(i).getAge() > maxAge) {
maxAge = list.get(i).getAge();
}
}
return maxAge;
}
}
测试类:
package sat03;
import java.util.ArrayList;
public class testmax {
public static void main(String[] args) {
//1.创建一个集合来存储
ArrayList
//2.创建3个学生对象
Student s1 = new Student("Zhangsan", 20, "男");
Student s2 = new Student("lisi", 23, "男");
Student s3 = new Student("wangwu", 25, "女");
//3.将学生对象添加到集合中
list.add(s1);
list.add(s2);
list.add(s3);
//4.调用方法
int max=StudentUtil.getMaxScore(list);
System.out.println(max);
}
}
3.static注意事项
静态方法只能访问静态变量和静态方法非静态方法可以访问静态变量或者静态方法,也可以访问非静态的成员变量和非静态的成员方法静态方法中是没有this关键字的
总结:
静态方法中,只能访问静态
非静态方法可以访问所有
静态方法中没有this关键字
4.重新认识main方法
public class HelloWorld{
public static void main (String[] args){
System.out.println("HelloWorld");
}
}
public : 被JVM调用,访问权限足够大
static :被JVM调用,不用创建对象,直接类名访问
因为main方法是静态的,所以测试类中其他方法也是需要是静态的
void : 被JVM调用,不需要给JVM返回值
main : 一个通用的名称,虽然不是关键字,但是被JVM识别
String[] args :以前用于接受键盘录入数据的,现在没用
九、继承
面向对象三大特征:封装、继承、多态
封装:对象代表什么,就得封装对应的数据,并提供数据对应的行为。
我们发现在Student类与Teacher类中有重复的元素,于是为了使程序更加便捷便出现了”继承“
1.继承概述
java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系
public class Student extends Person{}
Student称为子类(派生类),Person称为父类(基类或超类)
优点:
可以把多个子类中重复的代码抽取到父类中,提高代码的复用性子类可以在父类的基础上,增加其他的功能,使子类更强大
什么时候用继承?
当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码
2.继承的特点
java中只支持单继承,不支持多继承,但支持多层继承
单继承:一个子类只能继承一个直接父类
不支持多继承:子类不能同时继承多个父类
多层继承:子类A继承父类B,父类B可以继承父类C
C是A的间接父类
每一个类都直接或者间接的继承于Object
练习:
核心点:共性内容抽取,子类是父类中的一种
写代码是从父类开始写,最后写子类
JAVABean类
package st5;
public class Animal {
public void eat() {
System.out.println("我会吃饭");
}
public void water (){
System.out.println("我会喝水");
}
}
package st5;
public class Cat extends Animal {
public void mice() {
System.out.println("我会抓老鼠");
}
}
package st5;
public class Dog extends Animal {
public void lookhome() {
System.out.println("我会看家");
}
}
package st5;
public class Ragdoll extends Cat{
}
package st5;
public class Lihua extends Cat{
}
package st5;
public class Husky extends Dog{
public void breakhome() {
System.out.println("我会拆家");
}
}
package st5;
public class Teddy extends Dog{
public void Ceng(){
System.out.println("我喜欢蹭一蹭");
}
}
测试类:
package st5;
public class Test {
public static void main(String[] args) {
//创建对象并调用方法
//创建布偶猫的对象
Ragdoll rd = new Ragdoll();
System.out.println("我是布偶猫");
rd.mice();
rd.water();
rd.eat();
System.out.println("-------------------");
//创建狸花猫的对象
Lihua lh = new Lihua();
System.out.println("我是狸花猫");
lh.mice();
lh.water();
lh.eat();
System.out.println("-------------------");
//创建泰迪的对象
Teddy td = new Teddy();
System.out.println("我是泰迪");
td.lookhome();
td.water();
td.eat();
td.Ceng();
System.out.println("-------------------");
//创建哈士奇的对象
Husky hs = new Husky();
System.out.println("我是哈士奇");
hs.lookhome();
hs.water();
hs.eat();
hs.breakhome();
}
}
试运行:
注意:子类只能访问父类中非私有的成员
3.子类到底能继承父类中的哪些内容
构造方法非私有 不能private 不能成员变量非私有 能private 能 但不能直接用成员方法虚方法表 能否则 不能
虚方法表:就是经常要用的方法,什么叫虚方法表呢?非private 非static非final
4.继承中访问特点
继承中:成员变量的访问特点
public class Fu{
String name = "Fu";
}
public class Zi extends Fu{
String name = "Zi";
public void ziShow(){
String name = "ziShow";
System.out.println(name);
}
}
//就近原则:谁离我近,我就用谁
//完整版就近原则:先在局部位置找,本类成员位置找,父类成员位置找,逐级往上
//run:ziShow
如果出现了重名的成员变量怎么找:
System.out.println(name);//从局部位置开始往上找
System.out.println(this.name);//从本类成员位置开始往上找
System.out.println(super.name);//从父类成员位置开始往上找
public class Test{
public static void main (String [] args){
Zi z = new Zi();
z.ziShow();
}
}
public class Fu{
String name = "Fu";
}
public class Zi extends Fu{
String name = "Zi";
public void ziShow(){
String name = "ziShow";
System.out.println(name);//ziShow
System.out.println(this.name);//Zi
System.out.println(super.name);//Fu
}
}
继承中:成员方法的访问特点
直接调用满足就近原则:谁离我近,我就用谁
super调用,直接访问父类
package jicehng;
public class test {
public static void main(String[] args) {
//创建一个对象
Student s = new Student();
s.lunch();
/*
吃面条
咖啡
吃米饭
喝水
*/
}
}
class Person {
public void eat(){
System.out.println("吃米饭");
}
public void water(){
System.out.println("喝水");
}
}
class Student extends Person {
public void lunch(){
this.eat();//就近读取子类吃面条
this.water();//就近读取子类咖啡
super.eat();//调用父类吃米饭
super.water();//调用父类喝水
}
public void eat(){
System.out.println("吃面条");
}
public void water(){
System.out.println("咖啡");
}
}
方法的重写:
当父类的方法不能满足子类现在的需求时,需要进行方法重写
书写格式:
在继承体系中 ,子类出现了和父类中一模一样的方法声明,我们就称子类这个方法是重写的方法
@Override重写体系
@Override是放在重写后的方法上,校验子类重写时语法是否正确加上注解后如果有红色波浪线,表示语法错误建议重写方法都加@Override注解,代码安全,优雅!
方法重写注意事项和要求:
重写方法的名称、形参列表必须于父类中的一致子类重写父类方法时,访问权限子类必须大于等于父类(暂时了解:空着不写 JAVABean类: package jice; public class Dog { public void eat() { System.out.println("Dog is eating."); } public void drink() { System.out.println("Dog is drinking."); } public void lookhome() { System.out.println("Dog is lookhome."); } } package jice; public class hashiqi extends Dog { public void breakhome() { System.out.println("hashiqi is breakhome."); } } package jice; public class shapi extends Dog{ @Override public void eat() { super.eat();// 调用父类的eat方法 System.out.println("shapi is eating gouliang."); } } package jice; public class chinesedog extends Dog{ @Override public void eat() { super.eat();// 调用父类的eat方法 System.out.println("Chinesedog is eating chinesefood."); } } 测试类 package jice; public class test { public static void main(String[] args) { hashiqi hashiqi = new hashiqi(); hashiqi.eat(); hashiqi.drink(); hashiqi.lookhome(); shapi shapi = new shapi(); shapi.eat(); shapi.drink(); shapi.lookhome(); chinesedog chinesedog = new chinesedog(); chinesedog.eat(); chinesedog.drink(); } } 继承中:构造方法的访问特点 父类中的构造方法不会被子类继承 子类中所有的构造方法默认先访问父类中的无参构造,再执行自己 为什么? 子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据。子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化 怎么调用父类构造方法的? 子类构造方法的第一行语句默认都是:super(),不写也存在,且必须在第一行如果想调用父类有参构造,必须手动写super进行调用 例如代码实现: package sta08; public class Person { String name; int age; public Person() { System.out.println("父类的无参构造方法"); } public Person(String name, int age) { this.name = name; this.age = age; } } package sta08; public class Student extends Person { public Student() { //子类构造方法中隐藏的super()去访问父类的无参构造 super(); System.out.println("子类的无参构造方法"); } } package sta08; public class test { public static void main(String[] args) { //创建学生对象 Student stu = new Student(); } } this、super使用总结 this:理解为一个变量,表示当前发给发调用者的地址值; super:代表父类存储空间 关键字访问成员变量访问成员方法访问构造方法thisthis.成员变量 访问本类成员变量this.成员方法(…) 访问本类成员方法this(…) 访问本类构造方法supersuper.成员变量 访问父类成员变量super.成员方法(…) 访问父类成员方法super(…) 访问父类构造方法 package sta09; public class Employee { //1.类名见名知意 //2.属性私有 //3.构造方法(空参 带全部参数的构造) //4.get/set private String id; private String name; private double salary; public Employee() { } public Employee(String id, String name, double salary) { this.id = id; this.name = name; this.salary = salary; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } //方法 public void work() { System.out.println("员工在工作"); } public void eat(){ System.out.println("员工在吃饭吃米饭"); } } package sta09; public class Manager extends Employee{ private double bonus; public Manager() { } //带全部参数的构造 public Manager(String id, String name, double salary,double bonus) { super(id, name, salary); this.bonus = bonus; } public double getBonus() { return bonus; } public void setBonus(double bonus) { this.bonus = bonus; } //重写父类的方法 @Override public void work() { System.out.println("经理在管理其他人"); } } package sta09; public class Cook extends Employee{ public Cook() { } public Cook(String id, String name, double salary) { super(id, name, salary); } @Override public void work() { System.out.println("厨师在烹饪"); } } package sta09; public class Test { public static void main(String[] args) { //创建一个经理对象 Manager manager = new Manager("001","小明",100000,50000); System.out.println(manager.getId()+","+manager.getName()+ ","+manager.getSalary()+","+manager.getBonus()); manager.work(); manager.eat(); //创建一个厨师对象 Cook cook = new Cook("002","小红",80000); System.out.println(cook.getId()+","+cook.getName()+ ","+cook.getSalary()); cook.work(); cook.eat(); } } 十、多态 1.引言 面向对象三大特征之一:封装、继承、多态 有了封装才有了面向对象,有了面向对象才有了继承与多态 封装:对象代表什么,就得封装对应的数据,并提供数据对应的行为 随着业务的增加,这类对象越来越多,对象中重复的行为越来越多,于是我们把重复的内容提取出来,就有了子类和父类,这就是继承。我们用继承解决JavaBean中代码重复的问题,这也就是多态的前提条件。没有继承就没有多态。 2.多态 2.1 概览多态 什么是多态: 同类型的对象,表现出的不同形态 多态的表现形式: 父类类型 对象名称= 子类对象; 多态的前提: 有继承关系 有父类引用指向子类对象 有方法的重写 父类 package sta01; public class Person { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public void show() { System.out.println(name + " " + age); } } 子类 package sta01; public class Student extends Person { @Override public void show() { System.out.println("学生信息为:"+getName()+" "+getAge()); } } package sta01; public class Teacher extends Person { @Override public void show() { System.out.println("教师信息为:"+getName()+" "+getAge()); } } package sta01; public class Administrator extends Person { @Override public void show() { System.out.println("管理员信息为:"+getName()+" "+getAge()); } } 测试类 package sta01; public class test { public static void main(String[] args) { //创建三个学生对象 Student s1 = new Student(); s1.setName("小明"); s1.setAge(23); Student s2 = new Student(); s2.setName("小红"); s2.setAge(23); Student s3 = new Student(); s3.setName("小刚"); s3.setAge(23); //创建一个老师对象 Teacher t1 = new Teacher(); t1.setName("王老师"); t1.setAge(45); //创建一个管理员对象 Administrator a1 = new Administrator(); a1.setName("李管理员"); a1.setAge(23); //传递方法 register(s1); register(s2); register(s3); register(t1); register(a1); } //这个方法能接受老师,也能接受学生,还能接受管理员 public static void register(Person p) { p.show(); } } 多态的好处 使用父类型作为参数,可以接受所有子类对象,体现了多态的扩展性与便利 2.2 多态中调用成员的特点 变量调用:编译看左边,运行也看左边 方法调用:编译看左边,运行看右边 package sta02; public class test { public static void main(String[] args) { //创建对象(多态方式) Animal animal = new Dog(); //成员变量 //编译看左边,运行看左边 //编译看左边:java编译器在编译代码时,会检查左边的父类中有没有这个变量,如果有就通过编译,否则就会报错。 //运行看左边:在运行时,实际上是在执行父类中的变量,因为子类中没有这个变量,所以会输出父类中的变量值。 System.out.println(animal.name);//动物 //成员方法 //编译看左边,运行看右边 //编译看左边:java编译器在编译代码时,会检查左边的父类中有没有这个方法,如果有就通过编译,否则就会报错。 //运行看右边:在运行时,实际上是在执行子类中的方法,因为子类中重写了这个方法,所以会输出子类中的方法值。 animal.show();//Dog-----show方法 //理解 //Animal animal = new Dog(); //现在用animal类型的,所以默认都会从父类Animal中找 //成员变量:在子类的对象中,从父类的成员位置查找,无论是否有重写,都只看左边(父类) //成员方法:如果子类对方法进行了重写,那么在虚方法表中是会把父类的方法覆盖掉,只保留子类的方法, //所以在运行时,调用的是子类的方法,所以看右边(子类) } } class Animal { String name="动物"; public void show(){ System.out.println("Animal-----show方法"); } } class Dog extends Animal{ String name="狗"; @Override public void show(){ System.out.println("Dog-----show方法"); } } 程序内存图解说明: 1.类加载阶段(方法区准备) 程序运行前,Animal.class、Dog.class、Test.class 会被类加载器加载到 方法区: 存储类的元数据(成员变量、方法定义等 )。每个类会生成 “虚方法表”:Animal 虚方法表存 show();Dog 虚方法表因重写,show() 指向子类实现。 2.main 方法进栈(栈内存开始执行) Test 类的 main 方法作为程序入口,进入栈内存: 栈帧包含局部变量(如 Animal a )、操作数栈、方法出口等。执行 Animal a = new Dog(); 先在 堆内存 分配空间(地址 001 ),创建 Dog 对象:包含父类 Animal 继承的成员(name = "动物" )和自身成员(name = "狗" )。栈中 a 保存堆对象的引用(地址 001 )。 3. 执行 sout(a.name)(成员变量调用) 编译看左边:编译器检查 a 的声明类型是 Animal,所以找 Animal 类的 name。运行也看左边:直接访问栈中 a 关联的 Animal 成员变量(堆里 Animal 部分的 name = "动物" ),输出 动物。 4. 执行 a.show()(成员方法调用) 编译看左边:检查 a 声明类型 Animal,确认 Animal 有 show() 方法,编译通过。运行看右边:根据a实际引用的Dog对象,去Dog的虚方法表找 show()实现。 Dog 的 show() 方法 进栈,执行 System.out.println("Dog --- show方法"),输出 Dog --- show方法。方法执行完毕后,show() 栈帧 出栈。 5. 程序结束,栈帧出栈 main 方法执行完毕,其栈帧从栈内存弹出,程序结束。堆中对象后续由垃圾回收器(GC)根据可达性分析回收(若无引用关联)。 2.3 多态的优势 在多态形式下,右边对象可以实现耦合,便于扩展和维护 Person p = new Student () ; p.work() ; //业务逻辑发生改变时,后续代码无需修改 //假如,我想将Student改为Teacher,只需要把new Student () 改为new Teacher () 就可以了 定义方法的时候,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利 多态的弊端: 不能使用子类特有的功能,如果要强行使用子类的特有功能,要进行强行转换。 引用数据类型的类型转换,有几种方式? 自动类型转换、强制类型转换 Person p = new Student () ;//自动类型转换 Student s = (Student)p;//强制类型转换 强制类型转换能解决什么问题? 可以转换成真正的子类类型,从而调用子类独有的功能转换类型与真实对象类型不一致会报错转换的时候用instanceof关键字进行判断 // 定义父类:动物 abstract class Animal { public void eat() { System.out.println("动物在进食"); } // 抽象方法,由子类实现 public abstract void makeSound(); } // 子类:狗(有特有方法guardHouse) class Dog extends Animal { @Override public void makeSound() { System.out.println("狗在汪汪叫"); } // 狗特有的方法:看家 public void guardHouse() { System.out.println("狗在看家护院"); } } // 子类:猫 class Cat extends Animal { @Override public void makeSound() { System.out.println("猫在喵喵叫"); } } // 测试类 public class PolymorphismLimitationDemo { public static void main(String[] args) { // 多态:父类引用指向子类对象 Animal dog = new Dog(); Animal cat = new Cat(); // 调用父类方法(正常工作) dog.eat(); dog.makeSound(); // 尝试调用子类特有方法(编译错误!) // dog.guardHouse(); // 错误: 找不到符号 // 符号: 方法 guardHouse() // 位置: 类型为Animal的变量dog } } 通过强制转换解决弊端 public class PolymorphismCastingDemo { public static void main(String[] args) { // 多态引用 Animal dog = new Dog(); // 1. 安全的强制类型转换 if (dog instanceof Dog) { Dog realDog = (Dog) dog; // 强制类型转换 realDog.guardHouse(); // 访问子类特有方法 } //instanceof的用法 //他返回的是boolean值,左侧是要检查的对象引用,右侧是类名、接口名或是数组类型 // 2. 不安全的强制类型转换(会抛出异常) // Cat cat = (Cat) dog; // 运行时抛出ClassCastException // cat.makeSound(); //所以使用instanceof验证 // 3. 类型转换最佳实践 useDogFeatures(dog); } // 最佳实践:封装类型转换逻辑 public static void useDogFeatures(Animal animal) { if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.guardHouse(); dog.makeSound(); } else { System.out.println("这不是一只狗,无法执行看家功能"); } } } 练习: JavaBean类 package sta03; public class Animal { private int age; private String color; public Animal() { } public Animal(int age, String color) { this.age = age; this.color = color; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } /** * 获取 * @return color */ public String getColor() { return color; } /** * 设置 * @param color */ public void setColor(String color) { this.color = color; } public void eat(String something) { System.out.println("动物在吃:"+something); } } package sta03; public class Dog extends Animal{ //空参构造 //带上全部参数的构造 public Dog() { } public Dog(int age, String color) { super(age, color); } //行为 //eat(String something)(something是吃的东西) //看家lookHome() @Override public void eat(String something) { System.out.println(getAge()+"岁的"+getColor()+"颜色的狗两只前腿死死抱住"+something+"猛吃"); } public void lookHome(){ System.out.println("狗在看家"); } } package sta03; public class Cat extends Animal { public Cat() { } public Cat (int age, String color) { super(age, color); } @Override public void eat(String something) { System.out.println(getAge()+"岁的"+getColor()+"颜色的猫眯着眼睛吃"+something); } public void catchMouse(){ System.out.println("猫在抓老鼠"); } } package sta03; public class Person { private String name; private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } //饲养狗 public void keepPet(Dog dog, String something) { System.out.println("年龄为"+age+"岁的"+name+"在养一只"+dog.getColor()+"颜色的狗"+dog.getAge()+"岁的狗"); dog.eat(something); } //饲养猫 public void keepPet(Cat cat, String something) { System.out.println("年龄为"+age+"岁的"+name+"在养一只"+cat.getColor()+"颜色的猫"+cat.getAge()+"岁的猫"); cat.eat(something); } } 测试类 package sta03; public class test { public static void main(String[] args) { //创建对象并调用方法 Dog dog = new Dog(2, "黑色"); Person person1 = new Person("小明", 18); person1.keepPet(dog, "骨头"); Person person2 = new Person("小红", 20); Cat cat = new Cat(3, "白色"); person2.keepPet(cat, "鱼"); } } 十一、面向对象细节性知识点 1.包 什么是包? 包就是文件夹。用来管理各种不同功能的Java类,方便后期代码维护 包的命名规则 公司域名反写+包的作用,需要全部英文小写,见名知意。比如:com.ensichen.domain package com.ensichen.domain; public class Student { 私有化成员变量 构造方法 成员方法 } 包名+类名=全类名 com.ensichen.domain.Student或者叫全限定名 使用其他类的规则 使用其他类时要使用全类名 public class test { public static void main(String [] args ){ com.ensichen.domain.Student s = new com.ensichen.domain.Student(); } } 上面的写法太复杂于是就有了导包 import com.ensichen.domain.Student;//导包 public static void main(String [] args){ Student s = new Student () ; } 使用同一个包中的类时,不需要导包 public class test { public static void main(String[] args) { //1.使用同一个包中的类时,不需要导包 //创建对象 Student s = new Student(); s.setName("张三"); s.setAge(20); System.out.println(s.getName()+":"+s.getAge()); } } 使用java.lang包中的类时(JAVA核心包),不需要导包 public class test { public static void main(String[] args) { //2.使用java.lang包下的类时,不需要导包 String a = "abc"; System.out.println(a); //我们要找String字符串到底定义在哪个包中,我们可以按住Ctrl键点击String, // 或者选中String,然后按Ctrl键+鼠标左键,或者Ctrl+B就可以跳转到String的源码中 } } 其他情况都需要导包 如果同时使用两个包中的同名类,需要用全类名(用到的概率很小) 2.final关键字 final最终的——不可被改变的 final可以修饰方法、类、变量 方法: 表明该方法是最终方法,不能被重写 在父类的方法中我们用了final修饰,可我们会发现,子类中的重写方法会报错 类: 表明该类是最终类,不能被继承 我们用final修饰父类,我们就会发现子类报错 那我们的使用场景是什么呢? 当我们需要定义规则,不会被改变的时候就需要用到final 修饰类。Object类当中就有这样设计的。Ctrl+N 搜索Object我们就会看到final修饰的类 这里我们会发现没有方法体,实际上这里是用native,表示当前的方法体是调用本地汇编语言写的方法体。 变量: 叫做常量,只能被赋值一次 我们二次对a赋值时就会报错 这在一些类中也有应用,比如Math类中的pi是final修饰的固定值。 插播:行文至此,说到pi这与博主的网名有很大关系。博主的生日约等于pi的值,同一天也是爱因斯坦的生日。同理可得: 博主约等于爱因斯坦,并且博主真实姓名最后一个字叫做晨,故博主网名叫做爱因斯晨。是不是很有道理呢? 补充:常量 实际开发中,常量一般作为系统的配置信息,方便维护,提高可读性 常量的命名规范: 单个单词:全部大写多个单词:全部大写,单词之间用下划线隔开 细节: final修饰的变量是基本类型:那么变量存储的数据值不能发生改变。 final修饰的变量是引用类型:那么变量存储的地址值不能发生改变,对象内部可变。 核心:常量记录的数据是不能发生变化的 我们之前讲过,字符串里的内容是不能发生改变的,原因就是因为final private是私有的,外界不能获取。于是字符串靠pravite和final 使其不可修改. 3.权限修饰符 权限修饰符: 用来控制一个成员能够被访问的范围的 可以修饰成员变量,方法,构造方法,内部类 权限修饰符的分类: 有四种作用范围由小到大(private<空着不写 修饰符同一个类中同一个包中其他类不同包下的子类不同包下的无关类private√空着不写√√protected√√√public√√√√ private 私房钱,只能自己用 默认:只能本包用 protected 受保护的 public 公共的 权限修饰符的使用规则 实际开发中,一般只用private和public 成员变量是私有的 方法是公开的 特例:如果方法中的代码是抽取其他方法中共性代码,这个方法一般也私有 4.代码块 局部代码块 局部位置:方法里面的代码块。局部变量的作用范围,为了节约内存(提前结束变量的生命周期,已淘汰) 构造代码块 public class stu { private String name; private int age; public stu() { System.out.println("开始创建对象了"); } public stu(String name, int age) { this.name = name; this.age = age; System.out.println("开始创建对象了"); } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "stu{name = " + name + ", age = " + age + "}"; } } 在上述代码中我们看到了重复的代码,于是我们把它抽取放到局部代码块中,如下 public class stu { private String name; private int age; { System.out.println("开始创建对象了"); } public stu() { } public stu(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "stu{name = " + name + ", age = " + age + "}"; } } 构造代码块:写在成员位置的代码块 作用:可以把多个构造方法中重复的代码抽取出来 执行时机:我们在创建本类对象的时候会先执行构造代码块再执行 我们可以验证一下: public class stu { private String name; private int age; { System.out.println("开始创建对象了"); } public stu() { System.out.println("空参构造器"); } public stu(String name, int age) { this.name = name; this.age = age; System.out.println("有参构造器"); } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "stu{name = " + name + ", age = " + age + "}"; } } public class teest { public static void main(String[] args) { stu s1 = new stu(); stu s2 = new stu("zhnagsan", 20); } } 我们可以看到构造代码块优先执行于构造方法,但是这种方法不是很灵活,当我不想重复执行时就没有办法,于是,通常有了 重复代码我们都是:(不够灵活) 静态代码块 格式:static{} 特点:需要通过static关键字修饰,随着类的加载而加载,并且自动触发,只执行一次 使用场景:在类加载的时候,做一些数据初始化的时候使用 public class stu { private String name; private int age; static { System.out.println("静态代码块执行了"); } public stu() { System.out.println("空参构造器"); } public stu(String name, int age) { this.name = name; this.age = age; System.out.println("有参构造器"); } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "stu{name = " + name + ", age = " + age + "}"; } } public class teest { public static void main(String[] args) { stu s1 = new stu(); stu s2 = new stu("zhnagsan", 20); } } 执行时机:随着类的加载而加载,并且只执行一次,用于数据的初始化 十二、抽象类、接口、内部类 1.抽象类 引言: 当javabean的内容越来越多问题就出现了,于是就有了继承,将重复的属性提取出来 但是对象的一些行为不一样,方法体不一样,于是就有了抽象类 抽象方法 将共性的行为方法抽取到父类之后 由于每一个子类执行的内容不一样,所以,在父类中不能确定具体的方法体。该方法就可以定义为抽象方法 抽象类 如果一个类中存在抽象方法,那么该类就必须声明为抽象类 抽象类和抽象方法的定义格式 抽象方法的定义格式: public abstract 返回值类型 方法名(参数列表) 抽象类的定义格式: public abstract class 类名{} 抽象类和抽象方法的注意事项 抽象类不能实例化 package demo01; public abstract class Person { public abstract void sayHello(); } package demo01; public class test { public static void main(String[] args) { // 抽象类不能被实例化,只能被继承 Person person = new Person(); } } 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类 package demo01; public abstract class Person { //抽象类不一定有抽象方法 public void sayHello(){ System.out.println("Hello"); } } package demo01; public abstract class Person { //有抽象方法一定是抽象类 public abstract void sayHello(); } 可以有构造方法 抽象类的子类 要么重写抽象类中所有抽象方法 要么是抽象类 package sta02; public abstract class Animal { private String name; private int age; public Animal() { } public Animal(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public void drink(){ System.out.println("动物喝水"); } public abstract void eat(); } package sta02; public class Frog extends Animal{ public Frog() { } public Frog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("青蛙在吃虫子"); } } package sta02; public class Dog extends Animal{ public Dog() { } public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("狗在吃骨头"); } } package sta02; public class Sheep extends Animal { public Sheep() { } public Sheep(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("羊在吃草"); } } package sta02; public class test { public static void main(String[] args) { Frog frog = new Frog("小绿", 1); System.out.println(frog.getName() + " " + frog.getAge()); frog.eat(); frog.drink(); Dog dog = new Dog("小蓝", 2); System.out.println(dog.getName() + " " + dog.getAge()); dog.eat(); dog.drink(); Sheep sheep = new Sheep("小红", 3); System.out.println(sheep.getName() + " " + sheep.getAge()); sheep.eat(); sheep.drink(); } } 抽象类与抽象方法的意义 疑问把子类中共性的内容抽取到父类之后由于方法体不确定,需要定义为抽象。子类使用时需要重写那么我不抽取到父类,直接在子类中写不是更节约代码? 于是抽象方法让程序更好维护,更兼容 2.接口 为什么会有接口? 用来定义行为规范 接口与抽象类的异同 接口定义行为契约(能做什么),支持多重实现,适合解耦与组合;抽象类定义类型抽象(是什么),支持单继承,适合状态共享与模板复用。 接口的定义与使用 接口用关键字interface来定义 public interface 接口名{} 接口不能实例化 接口和类之间是实现关系,通过implements关键字表示 public class 类名 implements 接口名{} 接口的子类(实现类) 要么重写接口中的所有抽象方法 要么是抽象类 注意一:接口和类的实现关系,可以单实现,也可以多实现。 public class 类名 implements 接口名1,接口名2{} 注意二:实现类还可以在继承一个类的同时实现多个接口 public class 类名 extends 父类 implements 接口名1,接口名2{} 练习: 无接口写法: package demo01; public abstract class Animal { private String name; private int age; public Animal() { } public Animal(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public abstract void eat() ; } package demo01; public class frog extends Animal{ public frog() { } public frog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("青蛙在吃虫子"); } public void swimming() { System.out.println("青蛙在游泳"); } } package demo01; public class dog extends Animal{ public dog() { } public dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("狗在吃骨头"); } public void dig() { System.out.println("狗在刨地"); } } package demo01; public class rabbit extends Animal{ public rabbit() { } public rabbit(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("兔子在吃胡萝卜"); } } package demo01; public class test { public static void main(String[] args) { dog dog = new dog("小黑", 2); System.out.println(dog.getName() + "," + dog.getAge()); dog.eat(); dog.dig(); frog f = new frog("小绿", 1); System.out.println(f.getName() + "," + f.getAge()); f.eat(); f.swimming(); rabbit r = new rabbit("小红", 3); System.out.println(r.getName() + "," + r.getAge()); r.eat(); } } 加入接口的代码: 当我们新建接口时,应选择接口而不是类,新建之后,我们可以看出来,类与接口的图标表示不一样 详细代码: package demo02; public abstract class Animal { private String name; private int age; public Animal() { } public Animal(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public abstract void eat() ; } package demo02; public interface swim { public abstract void swimming(); } package demo02; public class Dog extends Animal implements swim{ public Dog() { } public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("狗吃骨头"); } @Override public void swimming() { System.out.println("狗会游泳"); } } package demo02; public class Frog extends Animal implements swim{ public Frog() { } public Frog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("青蛙吃虫子"); } @Override public void swimming() { System.out.println("青蛙会游泳"); } } package demo02; public class Rabbit extends Animal { public Rabbit() { } public Rabbit(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("兔子吃胡萝卜"); } } package demo02; public class test { public static void main(String[] args) { Dog dog = new Dog("小黑", 2); System.out.println(dog.getName()+" "+dog.getAge()); dog.eat(); dog.swimming(); Frog f = new Frog("小绿", 1); System.out.println(f.getName()+" "+f.getAge()); f.eat(); f.swimming(); Rabbit r = new Rabbit("小紫", 3); System.out.println(r.getName()+" "+r.getAge()); r.eat(); } } 接口中成员方法的特点 成员变量 只能是常量 默认修饰符:public static final 构造方法 没有 成员方法 只能是抽象方法 默认修饰符:public abstract JDK7以前:接口中只能定义抽象方法 JDK8的新特性:接口中可以定义有方法体的方法 JDK9的新特性:接口中可以定义私有方法 接口和类的关系 类和类的关系 继承关系,只能单继承,不能多继承,但是可以多层继承 类和接口的关系 实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口 接口与接口的关系 继承关系,可以单继承,也可以多继承 练习: // 说英语的接口 public interface SpeakEnglish { void speakEnglish(); } // 抽象类 - 人 public abstract class Person { private String name; private int age; public Person(String name, int age) { 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; } // 抽象方法,由子类实现具体行为 public abstract void doActivity(); } // 抽象类 - 运动员,继承 Person public abstract class Athlete extends Person { public Athlete(String name, int age) { super(name, age); } // 抽象方法,运动员的 “学习打球” 行为,子类具体实现 public abstract void learnPlayBall(); } // 抽象类 - 教练,继承 Person public abstract class Coach extends Person { public Coach(String name, int age) { super(name, age); } // 抽象方法,教练的 “教打球” 行为,子类具体实现 public abstract void teachPlayBall(); } // 乒乓球运动员,继承 Athlete 并实现 SpeakEnglish 接口 public class PingPongAthlete extends Athlete implements SpeakEnglish { public PingPongAthlete(String name, int age) { super(name, age); } // 实现运动员 “学习打球” 抽象方法 @Override public void learnPlayBall() { System.out.println(getName() + "(乒乓球运动员,年龄" + getAge() + ")正在学习打乒乓球"); } // 实现说英语接口方法 @Override public void speakEnglish() { System.out.println(getName() + "(乒乓球运动员)正在说英语交流"); } // 实现 Person 类的抽象方法 @Override public void doActivity() { learnPlayBall(); } } // 篮球运动员,继承 Athlete public class BasketballAthlete extends Athlete { public BasketballAthlete(String name, int age) { super(name, age); } // 实现运动员 “学习打球” 抽象方法 @Override public void learnPlayBall() { System.out.println(getName() + "(篮球运动员,年龄" + getAge() + ")正在学习打篮球"); } // 实现 Person 类的抽象方法 @Override public void doActivity() { learnPlayBall(); } } // 乒乓球教练,继承 Coach 并实现 SpeakEnglish 接口 public class PingPongCoach extends Coach implements SpeakEnglish { public PingPongCoach(String name, int age) { super(name, age); } // 实现教练 “教打球” 抽象方法 @Override public void teachPlayBall() { System.out.println(getName() + "(乒乓球教练,年龄" + getAge() + ")正在教打乒乓球"); } // 实现说英语接口方法 @Override public void speakEnglish() { System.out.println(getName() + "(乒乓球教练)正在说英语交流"); } // 实现 Person 类的抽象方法 @Override public void doActivity() { teachPlayBall(); } } // 篮球教练,继承 Coach public class BasketballCoach extends Coach { public BasketballCoach(String name, int age) { super(name, age); } // 实现教练 “教打球” 抽象方法 @Override public void teachPlayBall() { System.out.println(getName() + "(篮球教练,年龄" + getAge() + ")正在教打篮球"); } // 实现 Person 类的抽象方法 @Override public void doActivity() { teachPlayBall(); } } // 测试类,验证各个类功能 public class Test { public static void main(String[] args) { // 测试乒乓球运动员 PingPongAthlete pingPongAthlete = new PingPongAthlete("小张", 20); pingPongAthlete.doActivity(); pingPongAthlete.speakEnglish(); // 测试篮球运动员 BasketballAthlete basketballAthlete = new BasketballAthlete("小李", 22); basketballAthlete.doActivity(); // 测试乒乓球教练 PingPongCoach pingPongCoach = new PingPongCoach("王教练", 40); pingPongCoach.doActivity(); pingPongCoach.speakEnglish(); // 测试篮球教练 BasketballCoach basketballCoach = new BasketballCoach("赵教练", 45); basketballCoach.doActivity(); } } JDK8开始接口中新增的方法 JDK7以前:接口中只能定义抽象方法 JDK8的新特性:接口中可以定义有方法体的方法(默认、静态) 允许在接口中定义默认方法,需要使用关键字default修饰 作用:解决接口升级的问题 接口中默认方法的定义格式: 格式:public default 返回值类型 方法名 (参数列表){ }范例:public default void show ( ) { } 接口中默认方法的注意事项: 默认方法不是抽象方法,所以不强制被重写。但如果被重写,重写的时候去掉default关键字 public 可以省略,default不能省略 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写 // 接口定义 interface DefaultMethodInterface { // 默认方法(带方法体) default void defaultMethod() { System.out.println("这是接口中的默认方法"); } // 抽象方法(需实现类实现) void abstractMethod(); } // 实现类 class DefaultMethodImpl implements DefaultMethodInterface { @Override public void abstractMethod() { System.out.println("实现抽象方法"); } // 可选:重写默认方法 @Override public void defaultMethod() { System.out.println("重写默认方法"); } } // 测试类 public class DefaultMethodDemo { public static void main(String[] args) { DefaultMethodImpl obj = new DefaultMethodImpl(); obj.defaultMethod(); // 调用默认方法 obj.abstractMethod(); // 调用抽象方法 } } 允许在接口中定义定义静态方法,需要用static修饰 接口中静态方法的格式: 格式:public static 返回值类型 方法名 (参数列表){ } 范例:public static void show (){} // 接口定义 interface StaticMethodInterface { // 静态方法(带方法体) static void staticMethod() { System.out.println("这是接口中的静态方法"); } // 抽象方法 void abstractMethod(); } // 实现类 class StaticMethodImpl implements StaticMethodInterface { @Override public void abstractMethod() { System.out.println("实现抽象方法"); } } // 测试类 public class StaticMethodDemo { public static void main(String[] args) { StaticMethodImpl obj = new StaticMethodImpl(); obj.abstractMethod(); // 调用抽象方法 StaticMethodInterface.staticMethod(); // 只能通过接口名调用静态方法 } } 接口中静态方法的注意事项: 静态方法只能通过接口调用,不能通过实现类名或者对象名调用public 可以省略,static 不能省略 JDK9的新特性:接口中可以定义私有方法 接口中私有方法的定义格式: 格式1:private 返回值类型 方法名 (参数列表){ } 范例1:private void show ( ) { } // JDK9+ 接口(需 Java 9 及以上版本) interface PrivateInstanceMethodInterface { // 默认方法(调用私有实例方法) default void defaultMethod() { System.out.println("默认方法逻辑"); privateMethod(); // 调用私有实例方法 } // 私有实例方法(JDK9 特性) private void privateMethod() { System.out.println("这是接口中的私有实例方法"); } } // 实现类 class PrivateInstanceMethodImpl implements PrivateInstanceMethodInterface { // 无需实现私有方法,它们对实现类不可见 } // 测试类 public class PrivateInstanceMethodDemo { public static void main(String[] args) { PrivateInstanceMethodImpl obj = new PrivateInstanceMethodImpl(); obj.defaultMethod(); // 调用默认方法(间接调用私有方法) } } 格式2:private static 返回值类型 方法名 (参数列表){ } 范例2:private static void method ( ) { } // JDK9+ 接口 interface PrivateStaticMethodInterface { // 默认方法(调用私有静态方法) default void defaultMethod() { System.out.println("默认方法逻辑"); privateStaticMethod(); // 调用私有静态方法 } // 静态方法(调用私有静态方法) static void staticMethod() { System.out.println("静态方法逻辑"); privateStaticMethod(); // 静态方法只能调用静态私有方法 } // 私有静态方法(JDK9 特性) private static void privateStaticMethod() { System.out.println("这是接口中的私有静态方法"); } } // 实现类 class PrivateStaticMethodImpl implements PrivateStaticMethodInterface { // 无需实现私有方法 } // 测试类 public class PrivateStaticMethodDemo { public static void main(String[] args) { PrivateStaticMethodImpl obj = new PrivateStaticMethodImpl(); obj.defaultMethod(); // 调用默认方法 PrivateStaticMethodInterface.staticMethod(); // 调用静态方法 } } 适配器设计模式 设计模式是一套被反复使用、多数人知晓的,经过分类编目的、代码设计经验的总结,使用设计模式是为了可复用代码、让代码更容易被他人理解、保证代码可靠性,程序的重用性简单理解:设计模式就是各种套路适配器设计模式:解决接口与接口实现类之间的矛盾问题 总结: 当一个接口中抽象方法过多,但是我只要使用其中一部分的时候,就可以适配器设计模式书写步骤:编写中间类XXXAdapter,实现对应的接口;对接口中的抽象方法进行空实现;让真正的实现类继承中间类,并重写需要用的方法;为了避免其他类创建适配器类的对象,中间的适配器类用abstract进行修饰 3.内部类 类的五大成员之一:属性、方法、构造方法、代码块、内部类 什么是内部类? 在一个类的里面,在定义一个类。 举例:在A类的内部定义B类,B类就要称为内部类 为什么要学习内部类? 需求:写一个Javabean类描述汽车 属性:汽车的品牌,车龄,颜色,发动机的品牌,使用年限 public class Car{ String carName; int carAge; int carColor; String engineName; int engineAge; } public class Car{//外部类 String carName; int carAge; int carColor; clss Engine{//内部类 String engineName; int engineAge; } } 内部类 表示的事物是外部类的一部分 内部类单独出现没有任何意义 内部类的访问特点: 内部类可以直接访问外部类的成员,包括私有外部类要访问内部类的成员,必须创建对象 package demo03; public class Car { String name; String color; int age; public void show() { System.out.println(this.name); Engine engine = new Engine(); System.out.println(engine.engineType); } class Engine{ String engineType; int engineYears; public void show(){ System.out.println("发动机型号:"+engineType);//可以访问外部类的属性 System.out.println("车的名称"+name);//可以访问外部类的私有属性,加上private后,内部类也可以访问 } } } package demo03; public class test { public static void main(String[] args) { Car car = new Car(); car.name = "奔驰"; car.color = "黑色"; car.age = 1; car.show(); } } 内部类的分类 成员内部类 写在成员位置的,属于外部类的成员 public class Car{//外部类 String carName; int carAge; int carColor; clss Engine{//成员内部类 String engineName; int engineAge; } } 成员内部类可以被一些修饰符所修饰,比如:private, 默认,protected,public,static 等 在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。 获取成员内部类对象: 方法一:在外部类中编写方法,对外提供内部类的对象 方法二:直接创建格式:外部类名.内部类名 对象名=外部类对象. 内部类对象; public class BodyAndOrgans { public static void main(String[] args) { // 创建一个人的身体 Body person = new Body("张三"); // 通过身体获取心脏(方式一) Body.Heart heart = person.getHeart(); heart.pumpBlood(); // 直接创建肺(方式二) Body.Lung leftLung = person.new Lung("左肺"); leftLung.breathe(); // 身体运动,间接调用内部类方法 person.exercise(); } } class Body { private String name; private int oxygenLevel = 100; // 氧气水平 - 私有变量 public Body(String name) { this.name = name; System.out.println(name + "的身体已创建"); } // 成员内部类:心脏 public class Heart { public void pumpBlood() { System.out.println(name + "的心脏正在输送血液,使用氧气水平:" + oxygenLevel); // 直接访问外部类的私有变量 oxygenLevel -= 10; } } // 成员内部类:肺 public class Lung { private String position; public Lung(String position) { this.position = position; } public void breathe() { System.out.println(name + "的" + position + "正在呼吸,增加氧气水平"); // 直接访问外部类的私有变量和方法 oxygenLevel += 20; Body.this.checkOxygenLevel(); // 显式调用外部类方法 } } // 外部类方法:运动 public void exercise() { System.out.println(name + "开始运动..."); Heart heart = new Heart(); // 内部类可以在外部类方法中直接创建 heart.pumpBlood(); Lung rightLung = new Lung("右肺"); rightLung.breathe(); } // 外部类私有方法:检查氧气水平 private void checkOxygenLevel() { System.out.println("当前氧气水平:" + oxygenLevel); } // 对外提供心脏对象(方式一) public Heart getHeart() { return new Heart(); } } 静态内部类 静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的需要创建对象 public class Car {//外部类 String carName; int carAge; int carColor; static class Engine {//静态内部类 String engineName; int engineAge; } } 创建静态内部类对象的格式:外部类.内部类名 对象名 = new 外部类名.内部类名(); 调用非静态方法的格式:先创建对象,用对象调用 调用静态方法的格式:外部类名.内部类名.方法名(); 注意事项: 1.静态内部类也是成员内部类中的一种 2.静态内部类只能访问外部类中的静态变量和静态方法 ,如果想要访问非静态的需要创建对象。 创建静态内部类对象的格式: 外部类名.内部类名 对象名 = new 外部类名.内部类名(); 调用静态方法的格式: 外部类名.内部类名.方法名(); public class StaticInnerClassDemo { public static void main(String[] args) { // 创建静态内部类对象(无需先创建外部类对象) OuterClass.InnerClass inner = new OuterClass.InnerClass(); // 调用内部类方法 inner.printOuterStaticField(); // 访问外部类静态变量 inner.accessOuterNonStaticField(); // 通过创建外部类对象访问非静态成员 // 调用静态内部类的静态方法 OuterClass.InnerClass.printMessage(); } } class OuterClass { // 外部类的静态变量 public static String staticField = "外部类静态字段"; // 外部类的非静态变量 private String nonStaticField = "外部类非静态字段"; // 外部类的静态方法 public static void staticMethod() { System.out.println("外部类静态方法被调用"); } // 外部类的非静态方法 private void nonStaticMethod() { System.out.println("外部类非静态方法被调用"); } // 静态内部类(属于成员内部类的一种) static class InnerClass { // 静态内部类的静态变量 public static String innerStaticField = "内部类静态字段"; // 静态内部类的非静态变量 private int innerNonStaticField = 100; // 访问外部类静态成员 public void printOuterStaticField() { System.out.println("访问外部类静态字段: " + staticField); staticMethod(); } // 访问外部类非静态成员(需先创建外部类对象) public void accessOuterNonStaticField() { OuterClass outer = new OuterClass(); System.out.println("通过对象访问外部类非静态字段: " + outer.nonStaticField); outer.nonStaticMethod(); } // 静态内部类的静态方法 public static void printMessage() { System.out.println("静态内部类的静态方法被调用"); System.out.println("访问内部静态字段: " + innerStaticField); System.out.println("访问外部静态字段: " + staticField); staticMethod(); } } } 局部内部类 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量外界是无法直接使用,需要在方法内部创建对象并使用该类可以直接访问外部类的成员,也可以访问方法内的局部变量 public class StaticInnerClassDemo { public static void main(String[] args) { // 创建静态内部类对象(无需先创建外部类对象) OuterClass.InnerClass inner = new OuterClass.InnerClass(); // 调用内部类方法 inner.printOuterStaticField(); // 访问外部类静态变量 inner.accessOuterNonStaticField(); // 通过创建外部类对象访问非静态成员 // 调用静态内部类的静态方法 OuterClass.InnerClass.printMessage(); } } class OuterClass { // 外部类的静态变量 public static String staticField = "外部类静态字段"; // 外部类的非静态变量 private String nonStaticField = "外部类非静态字段"; // 外部类的静态方法 public static void staticMethod() { System.out.println("外部类静态方法被调用"); } // 外部类的非静态方法 private void nonStaticMethod() { System.out.println("外部类非静态方法被调用"); } // 静态内部类(属于成员内部类的一种) static class InnerClass { // 静态内部类的静态变量 public static String innerStaticField = "内部类静态字段"; // 静态内部类的非静态变量 private int innerNonStaticField = 100; // 访问外部类静态成员 public void printOuterStaticField() { System.out.println("访问外部类静态字段: " + staticField); staticMethod(); } // 访问外部类非静态成员(需先创建外部类对象) public void accessOuterNonStaticField() { OuterClass outer = new OuterClass(); System.out.println("通过对象访问外部类非静态字段: " + outer.nonStaticField); outer.nonStaticMethod(); } // 静态内部类的静态方法 public static void printMessage() { System.out.println("静态内部类的静态方法被调用"); System.out.println("访问内部静态字段: " + innerStaticField); System.out.println("访问外部静态字段: " + staticField); staticMethod(); } } } 匿名内部类(重点掌握) 匿名内部类本质上就是隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置 格式: new 类名或者接口名(){ 重写方法; } package demo04; public interface swim { public abstract void swim(); } package demo04; public class test { public static void main(String[] args) { new swim() { @Override public void swim() { System.out.println("重写了游泳的方法"); } }; } } 格式的细节: 包含了继承或实现,方法重写,创建对象 整体就是一个类的子类对象或者接口的实现类对象 使用场景: ``java public class Car {//外部类 String carName; int carAge; int carColor; static class Engine {//静态内部类 String engineName; int engineAge; } } 创建静态内部类对象的格式:`外部类.内部类名 对象名 = new 外部类名.内部类名();` 调用非静态方法的格式:先创建对象,用对象调用 调用静态方法的格式:`外部类名.内部类名.方法名();` 注意事项: 1.静态内部类也是成员内部类中的一种 2.静态内部类只能访问外部类中的静态变量和静态方法 ,如果想要访问非静态的需要创建对象。 创建静态内部类对象的格式: `外部类名.内部类名 对象名 = new 外部类名.内部类名(); ` 调用静态方法的格式: `外部类名.内部类名.方法名();` ```java public class StaticInnerClassDemo { public static void main(String[] args) { // 创建静态内部类对象(无需先创建外部类对象) OuterClass.InnerClass inner = new OuterClass.InnerClass(); // 调用内部类方法 inner.printOuterStaticField(); // 访问外部类静态变量 inner.accessOuterNonStaticField(); // 通过创建外部类对象访问非静态成员 // 调用静态内部类的静态方法 OuterClass.InnerClass.printMessage(); } } class OuterClass { // 外部类的静态变量 public static String staticField = "外部类静态字段"; // 外部类的非静态变量 private String nonStaticField = "外部类非静态字段"; // 外部类的静态方法 public static void staticMethod() { System.out.println("外部类静态方法被调用"); } // 外部类的非静态方法 private void nonStaticMethod() { System.out.println("外部类非静态方法被调用"); } // 静态内部类(属于成员内部类的一种) static class InnerClass { // 静态内部类的静态变量 public static String innerStaticField = "内部类静态字段"; // 静态内部类的非静态变量 private int innerNonStaticField = 100; // 访问外部类静态成员 public void printOuterStaticField() { System.out.println("访问外部类静态字段: " + staticField); staticMethod(); } // 访问外部类非静态成员(需先创建外部类对象) public void accessOuterNonStaticField() { OuterClass outer = new OuterClass(); System.out.println("通过对象访问外部类非静态字段: " + outer.nonStaticField); outer.nonStaticMethod(); } // 静态内部类的静态方法 public static void printMessage() { System.out.println("静态内部类的静态方法被调用"); System.out.println("访问内部静态字段: " + innerStaticField); System.out.println("访问外部静态字段: " + staticField); staticMethod(); } } } 局部内部类 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量外界是无法直接使用,需要在方法内部创建对象并使用该类可以直接访问外部类的成员,也可以访问方法内的局部变量 public class StaticInnerClassDemo { public static void main(String[] args) { // 创建静态内部类对象(无需先创建外部类对象) OuterClass.InnerClass inner = new OuterClass.InnerClass(); // 调用内部类方法 inner.printOuterStaticField(); // 访问外部类静态变量 inner.accessOuterNonStaticField(); // 通过创建外部类对象访问非静态成员 // 调用静态内部类的静态方法 OuterClass.InnerClass.printMessage(); } } class OuterClass { // 外部类的静态变量 public static String staticField = "外部类静态字段"; // 外部类的非静态变量 private String nonStaticField = "外部类非静态字段"; // 外部类的静态方法 public static void staticMethod() { System.out.println("外部类静态方法被调用"); } // 外部类的非静态方法 private void nonStaticMethod() { System.out.println("外部类非静态方法被调用"); } // 静态内部类(属于成员内部类的一种) static class InnerClass { // 静态内部类的静态变量 public static String innerStaticField = "内部类静态字段"; // 静态内部类的非静态变量 private int innerNonStaticField = 100; // 访问外部类静态成员 public void printOuterStaticField() { System.out.println("访问外部类静态字段: " + staticField); staticMethod(); } // 访问外部类非静态成员(需先创建外部类对象) public void accessOuterNonStaticField() { OuterClass outer = new OuterClass(); System.out.println("通过对象访问外部类非静态字段: " + outer.nonStaticField); outer.nonStaticMethod(); } // 静态内部类的静态方法 public static void printMessage() { System.out.println("静态内部类的静态方法被调用"); System.out.println("访问内部静态字段: " + innerStaticField); System.out.println("访问外部静态字段: " + staticField); staticMethod(); } } } 匿名内部类(重点掌握) 匿名内部类本质上就是隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置 格式: new 类名或者接口名(){ 重写方法; } package demo04; public interface swim { public abstract void swim(); } package demo04; public class test { public static void main(String[] args) { new swim() { @Override public void swim() { System.out.println("重写了游泳的方法"); } }; } } 格式的细节: 包含了继承或实现,方法重写,创建对象 整体就是一个类的子类对象或者接口的实现类对象 使用场景: 当方法的参数是接口或者类时,以接口为例,可以传递这个接口的实现对象,如果实现类只要使用一次,就可以用匿名内部类简化代码。