static
介绍
static表示静态,是Java中的一个修饰符可以修饰成员方法、成员变量
- 被static修饰的成员变量,叫做静态变量
- 被static修饰的成员方法,叫做静态方法
静态变量
特点:被该类所有对象共享
调用方式:
类名调用(推荐)
对象名调用
定义方式:
修饰符 static 数据类型 变量名 = 初始值;
老师是这么引入的,创建一个学生对象,
- 属性:姓名、年龄、性别
- 行为:学习
这个我们之前学过,那么一起跟着创建
Java Bean类代码如下:
package mystatic.a01staticdemo1;
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;
}
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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
// 行为
public void study(){
System.out.println(this.name + "正在学习");
}
public void show(){
System.out.println(this.name + "," + this.age + "," + this.gender );
}
}
test类代码如下
package mystatic.a01staticdemo1;
public class StudentTest {
public static void main(String[] args) {
// 创建第一个学生对象
Student s1 = new Student();
s1.setName("张三");
s1.setAge(23);
s1.setGender("男");
s1.study();
s1.show();
// 创建第二个学生对象
Student s2 = new Student();
s2.setName("李四");
s2.setAge(24);
s2.setGender("女");
s2.study();
s2.show();
}
}
执行这段代码之后:
张三正在学习
张三,23,男
李四正在学习
李四,24,女
这些都是符合预期的。
老师又添加了一个数学,教他的老师姓名。
那么添加之后的Javabean类是
package mystatic.a01staticdemo1;
public class Student {
private String name;
private int age;
private String gender;
public String teacherName;
public Student() {
}
public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
// 行为
public void study(){
System.out.println(this.name + "正在学习");
}
public void show(){
System.out.println(this.name + "," + this.age + "," + this.gender + "," + this.teacherName);
}
}
注意变量teacherName是public,不需要写get和set方法
修改了两处地方,添加了变量teacherName,还有修改show方法。
那么在test类中添加s1.teacherName = "阿玮老师";
。执行
张三正在学习
张三,23,男,阿玮老师
李四正在学习
李四,24,女,null
理论上李四,24,女,null
中为什么null
很好理解了,因为没有s2.teacherName = "阿玮老师";
但是问题来了,一个班的学生老师是共享的,所以李四的李四应该也是阿玮老师。s2.teacherName = "阿玮老师";
是可以解决问题的,这里就可以用静态变量来解决。
在刚刚在Student中添加的教师名称变量的代码: public String teacherName;
,改成public static String teacherName;
在test类中添加s1.teacherName = "阿玮老师";
那么运行代码
张三正在学习
张三,23,男,阿玮老师
李四正在学习
李四,24,女,阿玮老师
虽然只是在s1
中定义的变量,但是在s2
中也是可以使用、
被static修饰的变量可以让所有对象共享。虽然是在s1
中进行的赋值,但是s2
也共享teacherName="阿玮老师"
静态变量可以理解成固定死了。每个对象都可以用。
s1.teacherName = “阿玮老师”;还可以写成Student.teacherName = "阿玮老师";
直接用对象名.
的方式
内存图
修改代码如下:
package mystatic.a01staticdemo1;
public class Student {
String name;
int age;
static String teacherName;
public void show(){
System.out.println(this.name + "," + this.age + "," + teacherName);
}
}
package mystatic.a01staticdemo1;
public class StudentTest {
public static void main(String[] args) {
Student.teacherName = "阿玮老师";
// 创建第一个学生对象
Student s1 = new Student();
s1.name = "张三";
s1.age = 23;
s1.show();
// 创建第二个学生对象
Student s2 = new Student();
s2.show();
}
}
分别对应JavaBean类和测试类
静态区,存放被static修饰的变量。在jdk8以后,静态区放在堆内存里面。
上图是静态的内存图。默认两个show方法已经完成了进栈出栈。
一开始画内存图我都会有很大段的描述。现在,我觉得不需要很多描述了。易如反掌。这也体会到了进步。
需要注意在执行到Student.teacherName = "阿玮老师";
的时候,堆内存中是没有对象的、
随着类加载而加载,优先于对象。
静态方法
特点:
多用于测试类和工具类;
JavaBean类中很少用;
调用方式
类名调用(推荐)
对象名调用
静态变量是写在JavaBean类里面,一开始我以为静态方法也是这样的。但是根据之前写过的代码
package mystatic.a01staticdemo1;
public class Student {
String name;
int age;
static String teacherName;
public void show(){
System.out.println(this.name + "," + this.age + "," + teacherName);
}
}
在JavaBean类中,方法是没有用static修饰的,因此静态方法的意思不是在JavaBean类中描述方法的。
// 检查用户
public static int userExists(ArrayList<User> userList, String username){
for (int i = 0; i < userList.size(); i++) {
User u = userList.get(i);
String uName = u.getUsername();
// 找到这个用户
if (uName.equals(username)) return i;
}
return -1;
}
反观写过的测试类,方法是有static的。
静态方法也用于工具类。
- Javabean类:用来描述一类事物的类。比如:Student,Teacher,Dog,Cat等;
- 测试类:用来检查其他类是否书写正确,带有main方法的类,是程序的入口;
- 工具类:不是用来描述一类事物的,而是帮我们做一些事情的类;
又学了一种类。当听到这个名词的时候,我以为我学不懂了。
JavaBean类,我们老早就学过,面向对象,类是蓝图,对象是实例。把奥迪、宝马、大众等等这些都是车的牌子,只不过价格不同,性能不同。都是归根结底他们都是车。在制作的时候都要按照车的框架。因此这就是JavaBean来做的事情。把框架做好。
测试类,用于测试,测试JavaBean类写的对不对。工具类中的工具能不能用。有车的图纸了,那我要开始造车了。在测试类中含有main方法,程序的入口
工具类,帮助我们做一些事情,造车需要很多工具。
工具类的命名应做
- 类名到见名知意;
- 私有化构造方法,其目的不让外界创建他的对象
- 方法定义为静态;
public class ArrUtil {
private ArrUtil(){}
public static int getMax(...){...}
public static int getMin(...){...}
public static int getSum(...){...}
public static int getAvg(...){...}
}
类名ArrUtil,array是数组,util是工具的意思。那么这个类实现的是与数组相关的工具。 获取最大值、最小值、求和,求平均。
定义数组工具类
需求:
在实际开发中,经常会遇到一些数组使用的工具类;
请按照如下要求编写一个数组的工具类:ArrayUtil
提供一个工具类方法printArr,用于返回整数数组的内容。
返回的字符串格式如:[10,20,50,34,100 ](只考虑整数数组,且只考虑一维数组)
提供这样一个工具方法getAerage,用于返回平均分。(只考虑浮点型数组,且只考虑一维数组)
定义一个测试类TestDemo,调用该工具类的工具方法,并返回结果。
工具类代码如下:
package mystatic.a02staticdemo2;
public class ArrUtil {
// 私有化构造方法
private ArrUtil(){}
// 定义静态方法,方便调用。
public static String printArr(int[] arr) {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < arr.length; i++) {
if (i == arr.length-1) sb.append(arr[i]);
else sb.append(arr[i]).append(",");
}
sb.append("]");
return sb.toString();
}
public static double getAverage(double[] arr) {
double sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum / arr.length;
}
}
测试类代码如下:
package mystatic.a02staticdemo2;
public class TestDemo {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 3, 4};
String str = ArrUtil.printArr(arr1);
System.out.println(str);
double[] arr2 = {1.1, 2.2, 3.3, 4.4, 5.5};
double avg = ArrUtil.getAverage(arr2);
System.out.println(avg);
}
}
代码是没有什么难度的。我以为工具类的出现就是结构化代码。更方便阅读代码。
实际上这些事情之前都是写过的。全都写在了一个文件里面。把他们分开也增加了代码的阅读性。
定义学生工具类
需求:定义一个集合,用于存储3个学生对象。
学生类的属性为:name、age、gender
定义一个工具类,用于获取集合中最大学生的年龄。
JavaBean类:
package mystatic.a03staticdemo3;
public class Student {
// 学生类的属性为:name、age、gender
private String name;
private int age;
private char gender;
public Student() {
}
public Student(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
}
工具类:
package mystatic.a03staticdemo3;
import java.util.ArrayList;
public class StudentUntil {
private StudentUntil(){}
// 定义一个工具类,用于获取集合中最大学生的年龄。
public static int getMaxAge(ArrayList<Student> stuList){
int stuMaxAge = stuList.get(0).getAge(); // 设最大值是第一个学生
for (int i = 1; i < stuList.size(); i++) {
int stuAge = stuList.get(i).getAge();
if (stuAge > stuMaxAge) stuMaxAge = stuAge;
}
return stuMaxAge;
}
}
测试类:
package mystatic.a03staticdemo3;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<Student> stulist = new ArrayList<>();
Student stu1 = new Student("zhangsan", 24, '男');
Student stu2 = new Student("lisi", 25, '男');
Student stu3 = new Student("wangwu", 23, '男');
stulist.add(stu1);
stulist.add(stu2);
stulist.add(stu3);
int maxAge = StudentUntil.getMaxAge(stulist);
System.out.println(maxAge);
}
}
这种代码以前使用两个文件写的,现在变成三个文件了。多了一个工具类。而代码的逻辑都是一样的。求最值是在数组里面写过,这次换成了集合。
注意事项
- 静态方法只能访问静态变量和静态方法;
- 非静态方法可以访问静态变量或静态方法,也可以访问非静态的成员变量或非静态的成员方法;
- 静态方法中没有this关键字;
跟绕口令似的,说白了静态方法只能访问静态,而非静态方法都可以访问所有;静态方法中没有this。
代码方面理解
JavaBean类:
package mystatic.a04staticdemo4;
public class Student {
String name;
int age;
static String teacherName;
public void show1() {
System.out.println(name + ", " + age + ", " + teacherName);
}
public static void method(){
System.out.println("这是一个静态方法");
}
}
里面有一个静态方法和非静态方法。
test类:
package mystatic.a04staticdemo4;
public class Test {
public static void main(String[] args) {
Student.teacherName = "阿玮老师";
Student s1 = new Student();
s1.name = "zhangsan";
s1.age = 23;
s1.show1();
System.out.println("=============");
Student s2 = new Student();
s2.name = "lisi";
s2.age = 20;
s2.show1();
}
}
这段代码的运行是:
zhangsan, 23, 阿玮老师
=============
lisi, 20, 阿玮老师
Process finished with exit code 0
静态方法中没有this关键字,反过来就是非静态方法中有this关键字,存在于方法的形参中,这个形参不需要我们赋值,虚拟机自动赋值。
this指代调用者本身的地址,意思是在执行 Student s1 = new Student();
的时候,内存中会创建对象s1
的地址,反而 Student s2 = new Student();
会创建s2
是地址。
那么s1.show1()
的时候,this指代的是s1
的地址。s2.show1()
也是如此。
public void show1(Student this) {
System.out.println("this" + this);
System.out.println(name + ", " + age + ", " + teacherName);
}
改动show1方法。也打印S1和S2的地址System.out.println("s1" + s1);
和`System.out.println(“s2” + s2);
s1: mystatic.a04staticdemo4.Student@682a0b20
this: mystatic.a04staticdemo4.Student@682a0b20
zhangsan, 23, 阿玮老师
=============
s2mystatic.a04staticdemo4.Student@5b480cf9
this: mystatic.a04staticdemo4.Student@5b480cf9
lisi, 20, 阿玮老师
从输出中可以看出对象地址就等于this
也有了如下代码
public void show1(Student this) {
System.out.println("this: " + this);
System.out.println(this.name + ", " + this.age + ", " + teacherName);
}
this.成员变量
,this表示都对象,那么就是s1的name,s1的age,s2的name,s2的age。
teacherName不能用this,因为是静态变量,共享,不分你的我的
如果在Student类里面继续编写一个show2()
,让`show1()调用。
public void show2(){
System.out.println("show2()");
}
在show1()
里面可以用this调用,this.show2()
public void show1(Student this) {
System.out.println("this: " + this);
System.out.println(this.name + ", " + this.age + ", " + teacherName);
this.show2();
}
this.show2()
可以理解为用对象调用show2()
静态方法中没有this,即使手动添加形参this
public static void method(Student this){
System.out.println("this: " + this);
System.out.println("这是一个静态方法");
}
代码会报错。idea直接标红了。
Java为什么要这么设计,非静态的东西是和对象有关系的,你的就是你的,我的就是我的,他不会共享,就要分得很清。而静态的对象,共享,this是个代词,东西都共享了,也就不用特别指代了。
public static void method(){
System.out.println(name);
show1();
System.out.println("这是一个静态方法");
}
这段代码会报错,静态方法不能调用非静态的内容,不能调用非静态的方法和变量。即使是this.name
或者this.show1()
但是System.out.println(teacherName);
是允许的;
public void show1(Student this) {
System.out.println("this: " + this);
System.out.println(this.name + ", " + this.age + ", " + teacherName);
this.show2();
method();
}
在非静态方法中调用静态方法,是允许的。
以上就是代码角度,一开始对概念有点晕头转向,像绕口令。通过代码实践。显而易见了。
内存方面理解
- 静态:随着类加载而加载;、
- 非静态:跟对象有关;
静态内容和非静态内容的加载顺序不一样。就算是静态变量或方法会优先于非静态变量和方法出现的。创建对象之前肯定会先加载类,在加载类的时候,静态的内容就已经出来了。
不创建对象,那么非静态的内容就不会出现。让已经出现的静态内容找一个没有出现的非静态内容,这个肯定是找不到的。
静态方法不能访问非静态内容,
package mystatic.a04staticdemo4;
public class Student {
String name;
static String teacherName;
public void show() {
System.out.println(this.name + ", " + teacherName);
}
public static void method(){
System.out.println(name + ", " + teacherName);
}
}
package mystatic.a04staticdemo4;
public class Test {
public static void main(String[] args) {
Student.teacherName = "阿玮老师";
Student.method();
}
}
这段代码是有问题的,因为静态方法method不可以访问非静态变量name,接下来看一下,在内存中为什么不能访问。
方法区用来加载类,在执行Student.teacherName = "阿玮老师";
的时候,Student.class
就会加载到方法区。所有的成员变量和成员方法全部加载进来。在jdk以前,不管是静态还是非静态内容都会加载到方法区。jdk7以后,静态变量移到了堆内存。
执行method()
方法后。method()
入栈,执行method()
里的内容。System.out.println(name + ", " + teacherName);
就在这个时候,静态变量teacherName
可以在静态区里面找到,但是非静态变量name
怎么办呢,这个时候,只能报错了、
java: 无法从静态上下文中引用非静态 变量 name
非静态变量又叫实例变量,实例就是对象的意思。因此静态方法不能调用实例变量。
接下来,静态方法不能调用非静态的成员方法。
package mystatic.a04staticdemo4;
public class Student {
String name;
static String teacherName;
public static void method(){
System.out.println("静态方法" );
show();
}
public void show() {
System.out.println(this.name + ", " + teacherName);
}
}
package mystatic.a04staticdemo4;
public class Test {
public static void main(String[] args) {
Student.teacherName = "阿玮老师";
Student.method();
}
}
这段代码还是有问题的,静态方法method()
不能调用非静态方法show()
静态方法调用时不需要对象名的,如果在静态方法里面调用非静态方法,那么就是???.show()
不知道是谁调用的。
非静态可以访问所有。
package mystatic.a04staticdemo4;
public class Student {
String name;
int age;
static String teacherName;
public static void method(){
System.out.println("静态方法" );
}
public void show() {
System.out.println(this.name + ", " + teacherName);
}
}
package mystatic.a04staticdemo4;
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
s1.name = "张三";
s1.age = 23;
s1.show();
}
}
这段代码是正确的,也要明白非静态可以访问所有。
这个里面箭头有点乱。但是需要输出的变量都i能找到,因此代码没有问题。
非静态可以访问所有。
ackage mystatic.a04staticdemo4;
public class Student {
String name;
int age;
static String teacherName;
public static void method(){
System.out.println("静态方法" );
}
public void show() {
System.out.println(this.name + ", " + teacherName);
method();
}
}
package mystatic.a04staticdemo4;
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
s1.name = "张三";
s1.age = 23;
s1.show();
}
}
非静态方法调用静态,这段代码是没有问题的。
这也是能找到的。
重新认识main方法
public class helloWorld {
public static void main(String[] args) {
System.out.println("helloWorld");
}
}
-
public:被VM调用,访问权限足够大
-
static:被VM调用,不用创建对象,直接类名访问;
因为main方法是静态的,所以测试类中其他方法也需要是静态的。
-
void:被VM调用,不需要给VM返回值
-
main:一个通用的名称,虽然不是关键字,但是被VM识别
-
String[] args
:以前用于接收键盘录入数据的,现在没用
接下来详细说一下String[] args
一眼看去String[] args
是main的形参。这个参数默认情况下是没有东西的
public class helloWorld {
public static void main(String[] args) {
System.out.println(args.length); // 0
}
}
想要让它有输出,需要在idea里面配置。找到【Run/Debug Configurations】界面在【Program arguments】里面输入内容Hello World Java
,内容与内容之间用空格隔开。否则就是一个整体
public static void main(String[] args) {
System.out.println(args.length);
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
接下来这段代码就会有输出内容,内容是
3
Hello
World
Java
继承
继承介绍
从字面理解继承,父母的东西应该就是孩子。在Java中,也是这么一个理。
比如说开发一个校园管理系统。我需要设置两个类。学生类和教师类。
学生类有如下属性:学号、名字、年龄、地址;如下方法:吃饭、睡觉、学习和get/set
方法;
教师类有如下属性:工号、名字、年龄、地址;如下方法:吃饭、睡觉、教学和get/set
方法
虽然是两种不同的类,但是他们的属性和方法大部分是一样的。如名字、年龄、地址、吃饭、睡觉等
因此可以这么设计,把教师类和学生类共有的内容都放在一个类里面,然后让学生类和教师类继承这个类。
Java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系
public class Student extends Person {}
- Student称为子类(派生类)
- Person 称为父类(基类或者超类)
class 父类 {
...
}
class 子类 extends 父类 {
...
}
这是一个完整的格式。
派生类、基类、超类有点复杂。继承的关系大致就是父类、子类。
使用继承的好处
- 可以把多个子类中重复的代码抽取到父类中了,提高代码的复用性。
- 子类可以在父类的基础上,增加其他的功能,使子类更强大。
继承还是方便,同一个东西你就不用重复去写了。
子类继承父类的一切之后,还可以继续写自己的内容。在父类之上继续开发。
并不是任何时候都会使用继承,应满足如下条件
当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码
相同的部分可以放到父类去继承,但是不一样的部分就只能是各自了。就好比,把老师的教书方法放到父类,学生也就有了教学这个方法,不现实呀。学生不能有教学的方法。
继承的特点
Java支支持单继承,不支持多继承,但支持多层单继承
单继承:单,一个,意思是一个子类只有一个父类。不能有多个父类。因此不能多继承。
多层单继承呢,意思,A继承B,而B又继承C。是这么一个关系。在这里又会引出几个概念。
既然是这么个继承,那么C里的方法A也可以用、
- B是A的直接父类
- C是A的间接父类
每个类都直接或间接的继承于Object
C的上面会不会还有类呢,如果在代码中没有指明。那么继承于Object
从这里可以看出,面向对象的思维方式很符合实现世界。一个儿子只有一个父亲。儿子、父亲、爷爷的关系就是一个多层单继承。
Java有一个继承体系。儿子不能用他叔叔的东西、叔叔继承于爷爷。虽然爷爷是儿子的间接父类,但是儿子的直接父类是父亲。
练习题
现在有四种动物:布偶猫、中国狸花猫、哈士奇、泰迪。暂时不考虑属性,只要考虑行为。请按照继承的思想特点进行继承体系的设计。
四种动物分别有以下的行为:
- 布偶猫:吃饭、喝水、抓老鼠
- 中国狸花猫:吃饭、喝水、抓老鼠
- 哈士奇:吃饭、喝水、看家、拆家
- 泰迪:吃饭、喝水、看家、蹭一蹭
这么一堆内容,我本来准备上来直接写代码,但是老师说先画图。先画子类,再画父类。把子类共有的特性抽取到父类。
上面的图就是我们写的代码,在写代码的时候也应该注意一点:先写父类,后写子类。
代码如下
-
Animal.java
package myextends.a01exendsdemo01; public class Animal { public void eat(){ System.out.println("正在吃饭..."); } public void drink(){ System.out.println("正在喝水..."); } }
-
Cat.java
package myextends.a01exendsdemo01; public class Cat extends Animal{ public void catchMouse(){ System.out.println("正在抓老鼠..."); } }
-
Dog.java
package myextends.a01exendsdemo01; public class Dog extends Animal{ public static void bookHome(){ System.out.println("正在看家..."); } }
-
Ragdoll.java
package myextends.a01exendsdemo01; public class Ragdoll extends Cat{ }
-
LiHua.java
package myextends.a01exendsdemo01; public class LiHua extends Cat{ }
-
Husky.java
package myextends.a01exendsdemo01; public class Husky extends Dog { public void breakHome(){ System.out.println("正在拆家..."); } }
-
Teddy
package myextends.a01exendsdemo01; public class Teddy extends Dog{ public void touch(){ System.out.println("正在蹭一蹭..."); } }
-
Test.java
package myextends.a01exendsdemo01; public class Test { public static void main(String[] args) { Ragdoll rd = new Ragdoll(); rd.eat(); rd.drink(); rd.catchMouse(); System.out.println("============"); Husky h = new Husky(); h.eat(); h.breakHome(); h.breakHome(); } }
以上就是完整的代码。
父类的内容可以不继承。有些东西我不想给你用。因此权限可以设置成private
package myextends.a01exendsdemo01;
public class Animal {
private void eat(){
System.out.println("正在吃饭...");
}
private void drink(){
System.out.println("正在喝水...");
}
}
这样的话,父类的eta
和drink
方法子类就用不了。
子类继承父类哪些内容
父类的所有内容并不会完全继承给子类的,
子类不能继承父类的构造方法。
值得注意的是子类可以继承父类的私有成员(成员变量,方法),只是子类无法直接访问而已,可以通过getter/setter方法访问父类的private成员变量。
这段话是从老师的笔记上摘抄下来的,我的理解是父类中有的内容子类是不会继承的,什么情况下不会继承,我没有听明白。
- 父类的构造方法无论是非私有还是private,子类都不可以继承。
- 父类的成员变量是非私有还是private,子类都会继承
- 父类的方法变量只有非私有时子类可以继承,而private子类不可以继承。
内存图
构造方法是否可以被继承
构造方法是不能被继承的,无论是私有还是非私有。
为什么,构造方法必须要如类名同名。继承这件事本身就是两个不同类名的类完成的,那他还会继承吗
package myextends.a02exendsdemo02;
public class Test {
public static void main(String[] args) {
Zi z1 = new Zi();
Zi z2 = new Zi("张三", 210);
}
}
class Fu {
String name;
int age;
public Fu(){}
public Fu(String name, int age) {
this.name = name;
this.age = age;
}
}
class Zi extends Fu{
}
这段代码会报错。报错内容如下
E:\yang\专业\Java Code\basic-code\day13\src\myextends\a02exendsdemo02\Test.java:6:17
java: 无法将类 myextends.a02exendsdemo02.Zi中的构造器 Zi应用到给定类型;
需要: 没有参数
找到: java.lang.String,int
原因: 实际参数列表和形式参数列表长度不同
可以这么理解,
class Zi extends Fu{
public Fu(String name, int age) {
this.name = name;
this.age = age;
}
}
这段代码肯定不对呀。构造方法必须与类名一致。
Zi z1 = new Zi();
而这段代码并不是继承了父类的空参构造方法,而是Java虚拟机自动给子类Zi
创建的
如果一个类中没有构造方法,虚拟机会自动的给你添加一个默认的空参构造方法。
成员变量是否可以被继承
非私有的成员变量
分析如下代码的内存图
package myextends.a02exendsdemo02;
public class Test {
public static void main(String[] args) {
Zi z = new Zi();
System.out.println(z);
z.name = "钢门吹雪";
z.age = 23;
z.game = "王者荣耀";
System.out.println(z.name + ", " + z.age + ", " + z.game);
}
}
class Fu {
String name;
int age;
}
class Zi extends Fu{
String game;
}
这段代码的逻辑是很简单的,现在分析他的内存图。
这段代码的内存图如上图。
这里有两处不一样,第一处是方法区用来存放类的字节码文件。只要方法加载进来了,那么字节码文件就会被加载。因为是zi
类继承Fu
类,所以Fu类的文件要被加载进来。父类继承Object类,目前Object类对我们没有什么意义。因此不用深度了解。但是要知道Object类也会被加载。
第二处是堆内存,以前new出来的对象和这次nuw出来的对象。还是因为zi
类继承Fu
类,所以在创建对象的时候,会开辟两部分空间,一部分是继承父类的成员变量,另一部分是自己的成员变量。
被private修饰的成员变量
父类的成员变量被private修饰,子类可以继承,但是不能用
package myextends.a02exendsdemo02;
public class Test {
public static void main(String[] args) {
Zi z = new Zi();
System.out.println(z);
z.name = "钢门吹雪";
z.age = 23;
z.game = "王者荣耀";
System.out.println(z.game);
}
}
class Fu {
private String name;
private int age;
}
class Zi extends Fu{
String game;
}
代码在运行到z.name = "钢门吹雪";
会报错。
E:\yang\专业\Java Code\basic-code\day13\src\myextends\a02exendsdemo02\Test.java:7:10
java: name 在 myextends.a02exendsdemo02.Fu 中是 private 访问控制
通过内存图分析
理论上,程序在z.name = "钢门吹雪";
处就会停止。最终我还是画完了。
从堆内存的对象这来看,父类的成员变量被子类继承了,如果没有继承,根本不会给父类开辟空间。
当代码执行到z.name = "钢门吹雪";
时,因为父类的name由private修饰,因此就不能赋值。可以看到父类中的成员变量是初始值。而子类的成员变量game的值是王者荣耀。
可以是父类中的成员变量name
和age
被子类继承了,但是子类不能用。
成员方法是否可以被继承
老师说了个例子。我因为markdown图片需要图床,我懒得搭建了,所以我尽量少画图。
public class Test {
public static void main(String[] args) {
G g = new G();
g.methodA();
}
}
class A {
public void methodA(){ }
}
class B extends A{
public void methodB(){ }
}
class C extends B{
public void methodC(){ }
}
class D extends C{
public void methodD(){ }
}
class E extends D{
public void methodE(){ }
}
class F extends E{
public void methodF(){ }
}
class G extends F{
public void methodG(){ }
}
这段代码很变态,看着像是套娃。
最后我在Test类中创建了一个对象g
,然后我去调用methodA()
,这段代码是可以运行成功的,但是这里需要了解他是怎么运行的。
对象g
是G
类创建的,而G
类是由E
类继承下来的,而方法methodA()
是A
类的。G
类离A
类中间有6个间接子类。老师一开始这么说的时候,我以为虚拟机会一层一层向上找,最后找到我要执行的方法methodA()
,6个间接子类对代码的运行效率不显著,很快的。如果是600个呢,甚至更多呢
就像你在1楼坐电梯去100楼。你觉得需要花多少时间。
这里就提到了一个**创建对象调用方法的规则:**Java优化了这个需要,Java在顶级类(代码中的A类)设置一个虚方法表。
- 虚方法:不被
private
、static
和final
关键字修饰的方法; - 虚方法表:会把当前这个类中的虚方法单独抽取出来,放到虚方法表中。
在继承的应用,父类会把自己的虚方法表交给自己的子类。然后呢子类会在父类的虚方法表基础上添加自己的虚方法。然后又把虚方法表给子类的子类。
对应我上面的代码先是A
类把自己的虚方法添加到虚方法表中,然后又把这个表交给B
类,然后B
类会在A
类的基础上把自己的虚方法添加到虚方法表中,又交给C
类,C
类会在B
类和A
类的基础上添加后,交给D
类以此类推。
因此在执行g.methodA()
时,他不会一层一层的网上找,而是直接找G
类的虚方法表。找到需要调用的方法。如果需要调用的方法不是一个虚方法,那么他还是要一层一层网上找。
虚方法表有两个作用。
- 提升代码运行效率,
- 方法重写
提升代码效率是可以直观感受到的。而方法重写,后面会学。
只有父类中的虚方法才能被子类继承
package myextends.a02exendsdemo02;
public class Test {
public static void main(String[] args) {
Zi z = new Zi();
System.out.println(z);
z.ziShow();
z.fuShow1();
z.fuShow2();
}
}
class Fu {
public void fuShow1(){
System.out.println("public ... fuShow1");
}
private void fuShow2(){
System.out.println("private ... fuShow1");
}
}
class Zi extends Fu{
public void ziShow(){
System.out.println("public ... ziShow");
}
}
当然这段代码是错的,因为父类中fuShow2()
是被private修饰的不是虚方法,也不能继承。所以z.fuShow2();
语句有问题。
还是看一下他的内存图。
这一次把父类的父类Object.class
加载进来了。
当全部的类加载进来后,会出现一个虚方法表。在Object.class
中会有5个方法加入到虚方法表。然后Fu.class
会有一个虚方法加入虚方法表。因为方法fuShow2()
被private修饰,所以不会加入。然后是Zi.class
会有一个虚方法加入。
在执行zi.ziShow()
和zi.fuShow1()
方法时,因为这两个方法都存在于虚方法表中,因此很快就能找到。而方法fuShow2()
不在虚方法表中,会先在Zi.class
里面找,发现没有后,继续在他的父类中找,找到了。发现是被private修饰,不能用。
小总结
到这里,什么能继承,什么不能继承,通过内存图的方式就知道了
- 无论是私有的还是非私有的构造方法都不能继承。因为构造方法与类名一致。而子类和父类的类名必然不一致。
- 无论是私有还是非私有的成员变量,子类都可以继承,但是被private修饰的成员变量子类不能直接用。
- 如果成员方法可以被添加到虚方法表中,那么子类是可以继承的,反之,不能继承。
内存分析工具
接着,通过内存分析工具验证一下上面的结论。
继承中:成员变量的访问特点
有这么一段代码,
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中,输出的name是什么。
先说结论,最近原则
- 就近原则:谁离我近,我就用谁。
就近原则这个词语在前面学习过。根据上面的代码所示,System.out.println(name);
输出的是ziShow
先在局部找,本类成员位置找,父类成员位置找,逐级往上
如果要打印本类的成员变量name和父类的成员变量name呢,
- 打印本类的成员变量name可以是
this.name
- 打印父类的成员变量name可以是
super.name
对的。现在又学到一个关键字super
,如果要访问父类的父类,是不行的。
package myextends.a03exendsdemo03;
public class Test {
public static void main(String[] args) {
Zi z = new Zi();
z.ziShow();
}
}
class Fu {
String name = "Fu";
}
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
}
}
再说另一种情况
package myextends.a03exendsdemo03;
public class Test {
public static void main(String[] args) {
C c = new C();
c.cShow();
}
}
class A {
String name = "A";
}
class B extends A {
String name = "B";
}
class C extends B {
String name = "B";
public void cShow() {
String name = "CShow";
System.out.println(name);
System.out.println(this.name);
System.out.println(super.name);
System.out.println(super.super.name);
}
}
这段代码是错误的,子类最多能访问到父类,就像上面的代码,获取不到A
类的name
变量,也没有super.super.name
的用法。
继承中:成员方法的访问特点
依旧是就近原则,看如下代码
class Person {
public void eat() {
System.out.println("吃米饭,吃菜");
}
public void drink() {
System.out.println("喝水");
}
}
class Student extends Person {
public void lunch(){
// 先在本类中查看eat()和drink()方法,就会调用子类的,如果没有,就会调用从父类中继承下来的eat()和drink()方法
eat();
dink();
this.eat();
this.dink();
// 直接调用父类中的eat()和drink()方法
super.eat();
super.drink();
}
}
就这段代码而言,执行eat()
和this.eat()
是没有区别的。
子类中没有eat()
和drink()
方法,我以为执行this.eat()
会报错,但是不会报错,而且都指向父类。
class Person {
public void eat() {
System.out.println("吃米饭,吃菜");
}
public void drink() {
System.out.println("喝水");
}
}
class OverseasStudent extends Person {
public void lunch(){
this.eat(); // 吃意大利面
this.drink(); // 喝咖啡
super.eat(); // 吃米饭,吃菜
super.drink(); // 喝水
}
public void eat() {
System.out.println("吃意大利面");
}
public void drink() {
System.out.println("喝咖啡");
}
}
class Student extends Person {
public void lunch(){
this.eat(); // 吃米饭,吃菜
this.drink(); // 喝水
super.eat(); // 吃米饭,吃菜
super.drink(); // 喝水
}
}
这里又和上面的不一样了。
这里也就t提现到了就近原则。
在OverseasStudent
类中的lunch()
方法中,this.eat()
和super.eat()
就不一样了。触发了就近原则。因为在OverseasStudent
类中,我又重新写了eat()和drink()方法。
方法的重写
刚刚,我又重新写了eat()和drink()方法。这就是方法的重写,父类的方法不能满足子类的需求了
- 定义:当父类的方法不能满足子类现在的需求时,需要进行方法重写
- 格式:在继承体系中,子类出现了和父类中一模一样的方法声明,我们就城子类这个方法是重写的方法
@Override
重写注解
@Override
是放在重写后的方法上,校验子类重写时语法是否正确- 加上注解后如果有红色波浪线,表示语法错误
- 建议重写方法都加上
@Override
注解,代码更安全,优雅。
注解和注释的区别就是,注释是给我们看的,而注解不仅是给我们看,Java虚拟机还会看的。
package myextends.a04exendsdemo04;
public class Test {
public static void main(String[] args) {
OverseasStudent stu = new OverseasStudent();
stu.lunch();
}
}
class Person {
public void eat() {
System.out.println("吃米饭,吃菜");
}
public void drink() {
System.out.println("喝水");
}
}
class OverseasStudent extends Person {
public void lunch(){
this.eat(); // 吃意大利面
this.drink(); // 喝咖啡
super.eat(); // 吃米饭,吃菜
super.drink(); // 喝水
}
@Override
public void eat() {
System.out.println("吃意大利面");
}
@Override
public void drink() {
System.out.println("喝咖啡");
}
}
重写的本质
覆盖虚方法表,如果发生了重写,那么就会覆盖虚方法表中的虚方法。
重写发送在子父类,继承时,假设有一种继承结构
怎么解释呢,
每个类都有自己的虚方法表。
在这段继承结构中,最大的类是C
类,那么C
类的虚方法表就是C: method1()
和C: method2()
接着B
类继承C
类,然后B
类重写了method2()
,可以得出B
类的虚方法表就是C: method1()
和B: method2()
A
类继承B
类,A
类重写了method2()
,可以得出A
类的虚方法表就是C: method1()
和A: method1()
在这个时候的三种情况,我用代码演示说明
public class Test {
public static void main(String[] args) {
A a = new A();
a.method1();
a.method2();
}
}
当a.method1();
时,method1();
调用的是A类的虚方法表,正是C: method1()
当a.method2();
时,method2();
调用的是A类的虚方法表,正是A: method2()
public class Test {
public static void main(String[] args) {
B b = new b();
a.method2();
}
}
当b.method2();
时,method2();
调用的是B类的虚方法表,正是B: method2()
public class Test {
public static void main(String[] args) {
C a = new C();
c.method2();
}
}
当c.method1();
时,method2();
调用的是C类的虚方法表,正是C: method2()
重写的注意事项
- 重写方法的名称、形参列表必须与父类中的一致。
- 子类重写父类方法时,访问权限子类必须大于等于父类(暂时了解:空着不写 < protected < public)
- 子类重写父类方法时,返回值类型子类必须小于等于父类
- 建议:重写的方法尽量和父类保持一致。
- 私有方法不能被重写。
- 子类不能重写父类的静态方法,如果重写会报错的。
直接复制老师的,我应该不会犯错误,私有方法,你不能继承,那你就根本不能重写
直接是
只有被添加到虚方法表中的方法才能被重写
class Person {
public void eat() {
System.out.println("吃米饭,吃菜");
}
}
class OverseasStudent extends Person {
@Override
public void eat(int a) {
System.out.println("吃意大利面");
}
}
方法的重写,方法名必须是一致的,这个是不会出现错误的,但是我上面代码是有问题的,因为父类的eat()
方法没有形参,而子类的eat(int a)
方法含有形参。要不都有,要不都没有并且形参一致。
class Person {
public void eat() {
System.out.println("吃米饭,吃菜");
}
}
class OverseasStudent extends Person {
@Override
protected void eat() {
System.out.println("吃意大利面");
}
}
这段代码是有问题的
class Person {
protected void eat() {
System.out.println("吃米饭,吃菜");
}
}
class OverseasStudent extends Person {
@Override
public void eat() {
System.out.println("吃意大利面");
}
}
虽然权限还是不一样,但是这段代码是对的。
子类重写父类方法时,访问权限子类必须大于等于父类(暂时了解:空着不写 < protected < public)
public权限大,protected权限小。子类方法权限可以和父类不一样,但是必须要大于父类。
如果有以下继承关系
class Animal {}
class Dog extends Animal{}
class Cat extends Animal{}
从字面意思来说,动物类的范围肯定大于猫和狗。Dog类和Cat类同级都小于Animal类
父类的范围都是要比子类小的。父与子。子小
class Person {
public Animal eat() {
System.out.println("吃米饭,吃菜");
return unll;
}
}
class OverseasStudent extends Person {
@Override
public Dog eat() {
System.out.println("吃意大利面");
return unll;
}
}
虽然两个方法的返回值不一样,但是返回类型是Dog的小于Animal,所以代码正确。
如果反过来。
class Person {
public Dog eat() {
System.out.println("吃米饭,吃菜");
return unll;
}
}
class OverseasStudent extends Person {
@Override+
public Animal eat() {
System.out.println("吃意大利面");
return unll;
}
}
代码错误,父类的方法返回值类型必须要比子类大。
class Person {
private void eat() {
System.out.println("吃米饭,吃菜");
}
}
class OverseasStudent extends Person {
@Override
public void eat() {
System.out.println("吃意大利面");
}
}
父类的eat()方法被private修饰,私有的,都没有被加入虚方法表。那就更不可能被重写了。代码错误。
利用方法的重写设计继承结构
题目:
现在有三种动物:哈士奇、沙皮狗、中华田园犬。暂时不考虑属性,只要考虑行为。请按照继承的思想特点进行继承体系的设计。
三种动物分别有以下的行为:
哈士奇:吃饭(吃狗粮)、喝水、看家、拆家
沙皮狗:吃饭(吃狗粮、吃骨头)、喝水、看家
中华田园犬:吃饭(吃剩饭)、喝水、看家
先画类图,在进行程序编写
接着是代码设计
Dog.java
package myextends.a06exendsdemo06;
public class Dog {
public void eat() {
System.out.println("狗在吃狗粮");
}
public void drink() {
System.out.println("狗在喝水");
}
public void lookHome() {
System.out.println("狗在看家");
}
}
Husky,java
package myextends.a06exendsdemo06;
public class Husky extends Dog{
public void breakHome(){
System.out.println("哈士奇在拆家");
}
}
SharPei.java
package myextends.a06exendsdemo06;
public class SharPei extends Dog{
@Override
public void eat() {
super.eat(); // 吃狗粮
System.out.println("狗在啃骨头");
}
}
SharPei类把父类Dog类中的eat方法重写了、因为沙皮狗也会吃狗粮。会使用父类eat方法里面的代码。可以用笨方法复制代码,这里推荐super.eat()
ChineseDog.java
package myextends.a06exendsdemo06;
public class ChineseDog extends Dog{
@Override
public void eat() {
System.out.println("狗在吃剩饭");
}
}
DogTest.java
package myextends.a06exendsdemo06;
public class DogTest {
public static void main(String[] args) {
Husky h = new Husky();
h.eat();
h.drink();
h.lookHome();
h.lookHome();
SharPei sp = new SharPei();
sp.eat();
sp.drink();
sp.lookHome();
ChineseDog cd = new ChineseDog();
cd.eat();
cd.drink();
cd.lookHome();
}
}
运行结果:
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=51190:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" myextends.a06exendsdemo06.DogTest
狗在吃狗粮
狗在喝水
狗在看家
狗在看家
狗在吃狗粮
狗在啃骨头
狗在喝水
狗在看家
狗在吃剩饭
狗在喝水
狗在看家
Process finished with exit code 0
继承中:构造方法的特点
- 父类中的构造方法不会被子类继承
- 子类中所有的构造方法默认先访问父类中的无参构造,在执行自己。
两点中,第一点已经被验证过。
class Fu{
String name;
int age;
public Fu(){}
public Fu(String name, int age){
this.name = name;
this.age = age;
}
}
class Zi extends Fu{
public Zi(){}
}
比如说,执行代码Zi z = new Zi();
后,代码会先执行public Fu(){}
然后是public Zi(){}
为什么会这么设计
- 子类在初始化的生活,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类中的数据
- 子类初始化之前,一定要调用父类构造方法先完成数据空间的初始化。
其实是可以想明白的,继承嘛,你会把父类的一部分东西继承给子类,例如姓名、性别、年龄等。在赋值时,应该给父类先赋值这些内容。再说了,子类里面没有这些东西。通过子类访问这些数据时,z.name
name会在父类。不先给父类赋值,是找不到的。
- 子类构造方法的第一行语句默认都是:
super()
,不写也存在,且必须在第一行- 如果想调用父类的无参构造,必须手动写super进行调用
package myextends.a05exendsdemo05;
public class Test {
public static void main(String[] args) {
Zi z = new Zi();
}
}
class Fu {
String name;
int age;
public Fu() {
System.out.println("父类的无参构造方法");
}
public Fu(int age, String name) {
this.age = age;
this.name = name;
}
}
class Zi extends Fu{
}
大胆猜测一下,这段程序运行的结构是什么。
父类的无参构造方法
虽然我在Zi类里面什么都没有写,但是最后是有输出结果的。
当Zi类里面没有写任何构造方法时,会默认创建一个空参构造方法。而在Java虚拟机自动创建的空参构造里面的第一行语句是super();
类似于下面的代码
class Zi extends Fu{
public Zi() {
super();
}
}
接下来验证super语句必须是第一行。
public Zi() {
System.out.println("子类的无参构造");
super();
}
代码会报错,因为super();
必须在第一行。
package myextends.a05exendsdemo05;
public class Test {
public static void main(String[] args) {
Zi z = new Zi();
}
}
class Fu {
String name;
int age;
public Fu() {
System.out.println("父类的无参构造方法");
}
public Fu(String name, int age) {
this.age = age;
this.name = name;
}
}
class Zi extends Fu{
public Zi() {
super();
System.out.println("子类的无参构造方法");
}
}
这段代码的执行逻辑:
父类的无参构造方法
子类的无参构造方法
如果是用带参构造去创建对象呢,比如Zi z = new Zi("zhangsan", 23)
。那么首先就要在Zi类下面创建带参的构造方法,然后还是是有super(name, age)
public Zi(String name, int age) {
super(name, age)
}
完整代码:
package myextends.a05exendsdemo05;
public class Test {
public static void main(String[] args) {
Zi z = new Zi("zhangsan", 23);
System.out.println(z.name + ", " + z.age);
}
}
class Fu {
String name;
int age;
public Fu() {
System.out.println("父类的无参构造方法");
}
public Fu(String name, int age) {
this.age = age;
this.name = name;
System.out.println("父类的带参构造方法");
}
}
class Zi extends Fu{
public Zi() {
super();
System.out.println("子类的无参构造方法");
}
public Zi(String name, int age) {
super(name, age);
System.out.println("子类的带参构造方法");
}
}
执行结果是:
父类的带参构造方法
子类的带参构造方法
zhangsan, 23
代码的运行逻辑也就显而易见了。
当然也可以访问
this、super使用总结
- this:理解为一个变量,表示当前方法调用者的地址值
- super:代码父类存储空间。
this在前面的代码中是写过的,谁要去调用这个方法,那么谁的地址值就给了this。
而super用于访问父类的内容。
关键字 访问成员变量 访问成员方法 访问构造方法 this this.成员变量
访问本类成员变量this.成员方法(...)
访问本类成员方法this(...)
访问本类构造方法super super.成员变量
访问父类成员变量super.成员方法
访问父类成员方法`super(…)
访问父类构造方法
在这里,this()
咱们还没有了解。通过代码也是可以快速了解的。
package myextends.a06exendsdemo06;
public class Test {
public static void main(String[] args) {
Student stu = new Student();
stu.name = "zhangsan";
stu.age = 23;
System.out.println(stu.name + ", " + stu.age + ", " + stu.school);
}
}
class Student{
String name;
int age;
String school;
public Student() {
// 调用本类其他构造方法
// 细节:虚拟机不会在添加super
this(null, 0, "传智大学");
}
public Student(String name, int age, String school) {
this.name = name;
this.age = age;
this.school = school;
}
}
this(null, 0, "传智大学");
就相当于调用了全餐构造方法public Student(String name, int age, String school){}
因此this上面不能在用其他语句,this必须处于首句
public Student() {
System.out.println("111");
this(null, 0, "传智大学");
}
代码汇报错。原因是this(null, 0, "传智大学");
不处于句首。
这个的用于指定默认值,现在school的默认值就是"传智大学"。当然必需要空参构造方法、你用全参构造方法,school空的不写会报错
练习:带有继承结构的标准Javabean类
复习,什么是标准Javabean类
- 类名见名知意;
- 所有的成员变量都需要私有;
- 构造方法(空参带全部参数的构造)
- get/set
练习1
题目:
1.经理
成员变量:工号,姓名,工资,管理奖金
成员方法:工作(管理其他人),吃饭(吃米饭)
2.厨师
成员变量:工号,姓名,工资
成员方法:工作(炒菜),吃饭(吃米饭)
先绘制图
Employee.java
package myextends.a07exendsdemo07;
public class Employee {
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(int salary) {
this.salary = salary;
}
public void work(){
System.out.println("员工在工作");
}
public void eat(){
System.out.println("员工在吃米饭");
}
}
Manager.java
package myextends.a07exendsdemo07;
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("员工在管理员工");
}
}
Cook.java
package myextends.a07exendsdemo07;
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("厨师正在炒菜");
}
}
EmployeeTest.java
package myextends.a07exendsdemo07;
public class EmployeeTest {
public static void main(String[] args) {
Manager m = new Manager("001", "张三", 10000, 5000);
System.out.println(m.getId() + ", " + m.getName() + ", "
+ m.getSalary() + ", " + m.getBonus());
Cook c = new Cook("002", "李四", 8000);
System.out.println(c.getId() + ", " + c.getName() + ", " + c.getSalary());
}
}
在这里有个细节,虽然经理和厨师的work都被重写了,你为什么要还有抽取呢。
每个员工都要工作。如果还有其他员工呢,没有指定工作内容。那就调用默认的工作方法。
练习2
题目:
在黑马程序员中有很多员工(Employee)。按照工作内容不同分教研部员工(Teacher)和行政部员工(AdminStaff)
- 教研部根据教学的方式不同又分为讲师(Lecturer)和助教(Tutor)
- 行政部根据负责事项不同,又分为维护专员( Maintainer),采购专员(Buyer))
- 公司的每一个员工都编号,姓名和其负责的工作
- 每个员工都有工作的功能,但是具体的工作内容又不一样。
多态
多态的介绍
老师是这么引入的,学校的教务管理系统。大家都用过。学生可以登录,老师可以登录、管理员也可以登录。现在要写这个系统的注册业务逻辑。可以想到的是吧注册写成一个方法register()
。那么这个方法的形参又该怎么写。
老师让我们暂停视频自己思考。我是这么想的,形参无非就是用户名、密码、确认密码这些吗
public void register(String username, String password, String againPassword){
// 注册逻辑
}
这不是很简单嘛。但是老师是这么思考的,把整个学生对象放了进去。
public void register(Student s){
}
老师这么想是正确的,把前面的知识应该用起来。把用户名、密码等等这些数据封装起来。
问题来了,现在这个注册的方法只能是注册学生的,那老师和管理员怎么办。好办,方法可以重载
public void register(Teacher t){}
public void register(Admin a){}
这样是可以行得通的,如果还有其他角色,还要继续写下去吗。这样的话会很麻烦。
这里就用到了多态。多种形态、
public void register(Person p){}
这个的前提是学生、老师、管理员都继承人。
让这个注册方法的形参写成他们的父类person。这样不同的角色都可以注册了。前提的不同的角色都基础油person。
因此什么是多态
同类的对象,表现出不同的形态
多态的表现形式
父类类型 对象名称 = 子类对象
多态的前提
- 有继承或实现关系
- 有父类引入指向子类对象
- 有方法重写
第二条指向的意思是Fu f = new Zi()
,父类Fu通过等于执行子类对象Zi,实现是接口的内容,快要学了。
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.setName("张三");
s.setAge(18);
Teacher t = new Teacher();
t.setName("王建国");
t.setAge(30);
Administrator admin = new Administrator();
admin.setName("admin");
admin.setAge(35);
register(s);
register(t);
register(admin);
}
public static void register(Person p){
p.show();
}
}
class Person {
private String name;
private int age;
public Person() {
}
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 void show(){
System.out.println(this.name + ", " + this.age);
}
}
class Student extends Person{
@Override
public void show() {
System.out.println("学生的信息是: " + super.getName() + ", " + super.getAge());
}
}
class Teacher extends Person{
@Override
public void show() {
System.out.println("老师的信息是: " + super.getName() + ", " + super.getAge());
}
}
class Administrator extends Person{
@Override
public void show() {
System.out.println("管理员的信息是: " + super.getName() + ", " + super.getAge());
}
}
代码的运行结果是
学生的信息是: 张三, 18
老师的信息是: 王建国, 30
管理员的信息是: admin, 35
上面的代码就是应该多态形式。
在每个子类里面都重写了show方法。
可以理解成让注册方法是多种形态,不再是只能接收同一种形态的数据,
多态调用成员变量的特点
package polymorohism.a02polymorohismdemo02;
public class Test {
public static void main(String[] args) {
// 用多态方式创建对象 Fu f = new Z()l
Animal a = new Dog();
// 调用成员变量:编译看左边,运行也看左边
// 编译看左边:Javac编译代码的时候,会看左边的父类中有没有这个变量,如果有,编译成功,如果没有,编译失败。
// 运行也看左边:Java运行代码的时候,实际获取的就是左边父类中成员变量的值。
System.out.println(a.name); // 动物
// 调用成员方法,编译看左边,运行看右边
// 编译看左边:Javac编译代码的时候,会看左边的父类中有没有这个方法,如果有,编译成功,如果没有,编译失败。
// 运行也看左边:Java运行代码的时候,实际获取的是子类中的方法。
a.show(); // Dog...show
// 理解:
// Animal a = new Dog();
// 现在用a去调用变量和方法的呀?是的
// 而a是Animal类型的。所以默认都会从Animal这个类中去找
// 成员变量:在子类的对象中,会把父类的成员变量也继承下的,父:name,子:name
// 成员方法:如果子类对方法进行了重写,那么在虚方法表中是会把父类的方法进行覆盖的。
//
}
}
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");
}
}
class Cat extends Animal{
String name = "猫";
@Override
public void show(){
System.out.println("Cat...show");
}
}
这里搞得云里雾里。得出了这么一个结论是如果有的多态形式创建对象 Animal a = new Dog();
那么。a.name
中name是父类成员变量的数据,而a.show()
调用的是子类的成员方法。
从内存图角度分析
package polymorohism.a03polymorohismdemo03;
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.name); // 动物
a.show(); // Dog...show
}
}
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");
}
}
这个图,其实我感觉我没看出来。但是。大致就是这个意思。我理解到位了。
总之
- 调用成员变量的特点:编译看左边,运行也看左边
- 调用成员方法的特点:编译看左边,运行看右边
多态的优势和弊端
优势
在多态形势下,右边对象可以实现解耦合,便于扩展维护
这句话我没有理解,但是老师说的例子我明白了。
Person p = new Student();
p.work();
p.work
实际上调用的是Student里的work方法。学生工作
如果过几天,我想改成老师工作,那么就只需要吧new Student()
改成new Teacher()
,这么一来,是不是简单了。
定义方法的时候,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展与便利
这条在上面体现过。
public static void register(Person p){
p.show();
}
在学习字符串的时候,学习过StringBuilder对象
StringBuilder sb = new StringBuilder();
当在idea中sb.append();
的时候,会有一个append(Object obj)
的提示,这就是多态的体系。
所有类都会继承Object,那么这个函数就可以接收不同的对象
StringBuilder sb = new StringBuilder();
sb.append(a);
System.out.println(sb); // polymorohism.a03polymorohismdemo03.Dog@41629346
程序运行是正确的,但是这个结果我是看不懂。
还有集合,我们之前是这么使用的
ArrayList <String> list = new ArrayList<>();
其实可以不用指定泛型,
ArrayList list = new ArrayList();
那么在idea编码的时候list.add()
这个里面的形参就是Object。
ArrayList list = new ArrayList();
list.add(a);
System.out.println(list); // [polymorohism.a03polymorohismdemo03.Dog@41629346]
代码运行成功
弊端
package polymorohism.a04polymorohismdemo04;
public class Test {
public static void main(String[] args) {
// 多态方式创建对象
Animal a = new Dog();
// 编译看左边,运行看右边
a.eat(); // 狗吃骨头
// 多态的弊端
// 不能调用子类特有功能
// 原因,当调用成员方法时, 编译看左边,运行看右边
// 那么在编译的时候会先检查左边的父类中有没有这个方法。没有直接报错
// a.lookHome();
// 解决方法
// 变回子类类型即可
// Dog d = (Dog) a;
// d.lookHome(); // 狗在看家
// 细节,转换的时候不能瞎转,如果转换成其他类的类型会报错
// Cat c = (Cat) a; // class polymorohism.a04polymorohismdemo04.Dog cannot be cast to class polymorohism.a04polymorohismdemo04.Cat
// 判断a是不是Dog类型
// if (a instanceof Dog){
// Dog d = (Dog) a;
// d.lookHome();
// } else if (a instanceof Cat) {
// Cat c = (Cat) a;
// c.catchMouse();
// } else {
// System.out.println("没有这个类型");
// }
// 新特性
// 先判断a是不是Dog类,如果是,强转成Dog类型,转换之后变量名为d
// 如果不是不强转,结果是false
if (a instanceof Dog d){
d.lookHome();
} else if (a instanceof Cat c) {
c.catchMouse();
} else {
System.out.println("没有这个类型");
}
}
}
class Animal{
public void eat(){
System.out.println("动物在吃东西");
}
}
class Dog extends Animal{
@Override
public void eat() {
System.out.println("狗吃骨头");
}
public void lookHome(){
System.out.println("狗在看家");
}
}
class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫吃小鱼干");
}
public void catchMouse(){
System.out.println("猫在抓老鼠");
}
}
多态的弊端是不能调用子类特有的方法。因为父类没有子类特有的方法
当调用成员方法时, 编译看左边,运行看右边
这句话是要牢记的。如果我非用不可怎么办。强转,
Dog d = (Dog) a;
但是强转并不是什么都能转,要遵循规则。创建的是狗,如果强转成猫,会报错
Exception in thread "main" java.lang.ClassCastException: class polymorohism.a04polymorohismdemo04.Dog cannot be cast to class polymorohism.a04polymorohismdemo04.Cat (polymorohism.a04polymorohismdemo04.Dog and polymorohism.a04polymorohismdemo04.Cat are in unnamed module of loader 'app')
at polymorohism.a04polymorohismdemo04.Test.main(Test.java:22)
上面的内容是报错日志。
这种问题可以避免。我也不知道a是什么类型。
if (a instanceof Dog d){
d.lookHome();
} else if (a instanceof Cat c) {
c.catchMouse();
} else {
System.out.println("没有这个类型");
}
这个是jdk14的新特性。先判断a是不是Dog类,如果是,强转成Dog类型,转换之后变量名为d
包
包的介绍
包就是文件夹,用来管理各种不同功能的Java类,方便后期代码维护。
在python中也存在这个概念。这是一种代码的组织方式。比如说做个应用。你不可能吧全部代码放在一个文件里吧,也不可能把几个文件把中项目的根目录把、这时就需要包。
包名的规则:公司域名反写 + 包的作用,需要全部英文小写,见名知意。
com.itheima.domain
我没有公司域名,咱们中做代码练习的时候,基本上就是domain
清楚这个包做什么就行了。
package com.itheima,domein;
public class Student {
}
在我之前写的代码的时候,一直搞不懂package com.itheima,domein;
是什么意思,idea自动添加。现在就是可以解释了。意思是Student类存在于com.itheima,domein这个包
然后之前使用这个类的时候,咱们的代码是
Student stu = new Student();
现在多了一种方式
com.itheima,domein.Student stu = new com.itheima,domein.Student();
这种方式调用有个新名词,全类名或者全限定名。
使用其他类的规则
public class Test {
public static void main(String[] args) {
com.itheima,domein.Student stu = new com.itheima,domein.Student();
}
}
在使用其他类时需要用全类名。但是这么写属实麻烦。因此出了一个新名词,导包、
import com.itheima,domein.Student;
public class Test {
public static void main(String[] args) {
Student stu = new Student();
}
}
import com.itheima,domein.Student;
这条代码也很常见,idea会自动添加。
- 使用同一个包中的类时,不需要导包。
- 使用java.lang包中的类时,不需要导包
- 其他情况都需要导包
- 如果同时使用两个包中的同名类,需要用全类名。
final
final是最终的意思,被修饰的内容不可以改变。可以修饰方法、类、变量。
final修饰方法
如果被final修饰的方法,这个方法是最终方法;就不可以重写了。
package myfinal.a01finaldemo01;
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
class Fu{
public final void show(){
System.out.println("父类的show()方法");
}
}
class Zi extends Fu{
@Override
public void show(){
System.out.println("子类的show()方法");
}
}
这段代码是有问题的,
E:\yang\专业\Java Code\basic-code\day13\src\myfinal\a01finaldemo01\Test.java:17:17
java: myfinal.a01finaldemo01.Zi中的show()无法覆盖myfinal.a01finaldemo01.Fu中的show()
被覆盖的方法为final
看他的报错内容,意思就是被final修饰的方法不能被重写
这个用于一些不写被别人改变的方法。用final
final修饰类
如果被final修饰的类,最终类,这个类不可以继承了。
package myfinal.a01finaldemo01;
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
final class Fu{
public void show(){
System.out.println("父类的show()方法");
}
}
class Zi extends Fu{
@Override
public void show(){
System.out.println("子类的show()方法");
}
}
这段代码与上面的代码是相同的,但是现在修饰的Fu类。代码还是会报错
E:\yang\专业\Java Code\basic-code\day13\src\myfinal\a01finaldemo01\Test.java:15:18
java: 无法从最终myfinal.a01finaldemo01.Fu进行继承
最终类,不能被继承
final修饰变量
如果被final修饰的变量,常量,只能赋值一次
public class Test {
public static void main(String[] args) {
final int a = 10;
a = a+1;
System.out.println(a );
}
}
这段代码会报错
E:\yang\专业\Java Code\basic-code\day13\src\myfinal\a01finaldemo01\Test.java:6:9
java: 无法为最终变量a分配值
但是这个a是可以打印的。也可以使用
public class Test {
public static void main(String[] args) {
final int a = 10;
// a = a+1;
System.out.println(a); // 10
System.out.println(a + 1); // 11
}
}
实际开发中。常量一般作为系统的配置信息,便于维护,提高可读性
常量的命名规范:
- 单个单词:全部大写
- 多个单词:全部大写,单词之间用下划线隔开
细节:
- final修饰的变量是基本类型:那么变量存储的数据值不能发生改变。
- final修饰的变量是引用类型:那么变量存储的地址值不能发生改变,对象内部的可以改变。
final修饰基本数据类型的细节已经体现过了。被final修饰的变量不能发生改变。
而final修饰引用数据类型,地址值不该,但是对象的内部(属性)可以改变
package myfinal.a02finaldemo02;
public class Test {
public static void main(String[] args) {
final Student stu = new Student("张三", 20);
System.out.println(stu.getName() + ", " + stu.getAge()); // 张三, 20
stu.setName("李四");
stu.setAge(25);
System.out.println(stu.getName() + ", " + stu.getAge()); // 李四, 25
// stu = new Student(); // java: 无法为最终变量stu分配值
final int[] ARR = {1,2,3,4,5};
ARR[0] = 11;
ARR[5] = 55;
// ARR = new int[10]; // java: 无法为最终变量ARR分配值
}
}
class Student {
private String name;
private int age;
public Student() {
}
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;
}
}
很好理解。在执行stu.setName("李四");
、stu.setAge(25);
、 ARR[0] = 11;
和ARR[5] = 55;
的时候,stu还是ARR他们保存的地址值没有发生变化、
但是new的话,就会开辟新的内存,地址值发送改变了,那就不行了。
权限修饰符
- 权限修饰符:是用来控制有个成员能够被访问的范围的
- 可以修饰成员变量,方法,构造方法,内部类。
这个名词是刚刚遇到的,但是这个功能咱们是从一开始就用到的
public class Student{
private String name;
private int age;
}
这段代码中,public、private就是权限修饰符。
在Java中一共有四种权限修饰符。通过作用范围从小到大的顺序排列是private
< 空着不写(缺省/默认)
< protected
< public
。有这么一张表格
修饰符 | 同一个类中 | 同一个包中其他类 | 不同包下的子类 | 不同包下的无关类 |
---|---|---|---|---|
private | √ | |||
空着不写 | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
这个表格应该怎么理解呢。在上面已经学习过包了、private范围最小,而public范围最大
在开发中遵循一下规则即可
实际开发中,一般只用orivate和oublic
- 成员变量私有
- 方法公开
特例:如果方法中的代码是抽取其他方法中共性代码,这个方法一般也私有。
代码块
什么是代码块,老师是这么演示的
public class Test {
public static void main(String[] args) {
{
int a = 10;
System.out.println(a); // 10
}
}
}
代码块根据位置的不同分为
- 可以写在方法里面的局部代码块
- 可以写在成员位置也就是方法外类里面的构造代码块
- 加上static进行修饰的静态代码块
局部代码块
听着很高大上,但是已经淘汰了。
public class Test {
public static void main(String[] args) {
{
int a = 10;
System.out.println(a); // 10
}
}
}
这就是局部代码块的应用。
为什么说是淘汰了。这对大括号的作用就是提前结束变量的生命周期。咱们之前学习过,变量的作用范围只在所属的范围内有效。
package mycodeblock;
public class CodeBlockDemo1 {
public static void main(String[] args) {
{
int a = 10;
}
System.out.println(a);
}
}
这段代码会发生错误,原因是找不到a,虽然是定义a了。定义的 a在大括号里面,而我的sout语句在大括号外面。
E:\yang\专业\Java Code\basic-code\day13\src\mycodeblock\CodeBlockDemo1.java:8:28
java: 找不到符号
符号: 变量 a
位置: 类 mycodeblock.CodeBlockDemo1
提前结束变量生命周期。防止内存占用。但是现在的内存很大了。根本不用考虑这个问题了。
构造代码块
class Student02{
private String name;
private int age;
public Student02() {
System.out.println("开始创建对象了");
}
public Student02(String name, int age) {
System.out.println("开始创建对象了");
this.name = name;
this.age = age;
}
}
这段代码想要做什么呢。就是在创建对象的时候都想要输出一句开始创建对象了。无论是空参构造方法还是有参。
上面的代码是在每一个构造方法里面写入了一句开始创建对象了。如果要插入很多行语句呢,这么写的话,代码可读性不高,因此需要改这些代码
class Student02{
private String name;
private int age;
{
System.out.println("开始创建对象了");
}
public Student02() {
}
public Student02(String name, int age) {
this.name = name;
this.age = age;
}
}
构造代码块有如下逻辑
构造代码块:
- 写在成员位置的代码块
- 作用:可以把多个构造方法中重复的代码抽取出来
- 执行时机:我们在创建本类对象的时候会先执行构造代码块再执行构造方法
我们应该注意他的有限时机,构造代码块会先执行,然后是构造方法
package mycodeblock;
public class CodeBlockDemo2 {
public static void main(String[] args) {
Student02 stu0201 = new Student02();
Student02 stu0202 = new Student02("张三", 22);
}
}
class Student02{
private String name;
private int age;
{
System.out.println("开始创建对象了");
}
public Student02() {
System.out.println("空参构造方法");
}
public Student02(String name, int age) {
System.out.println("有参构造方法");
this.name = name;
this.age = age;
}
}
这段代码的输出结果如下
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=55835:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" mycodeblock.CodeBlockDemo2
开始创建对象了
空参构造方法
开始创建对象了
有参构造方法
Process finished with exit code 0
这个技术也不常用了。如果我还写一个构造方法。但是我不希望执行开始创建对象了。这个时候就要回到原始,
如果我们要写多个构造方法,其中有几个代码块中有重复的语句。怎么办。不难
第一种方法就是咱妈学过的,把重复的代码抽取成方法。
class Student02{
private String name;
private int age;
{
}
public Student02() {
调用方法();
}
public Student02(String name, int age) {
调用方法();
this.name = name;
this.age = age;
}
public Student02(String name) {
this.name = name;
}
}
还有一个用this。在前面学习过, this(...)
访问本类构造方法,把相同的代码块给其中应该构造方法。
class Student02{
private String name;
private int age;
public Student02() {
this(null, 0); // 不知道值。默认
}
public Student02(String name, int age) {
System.out.println("开始创建对象了");
this.name = name;
this.age = age;
}
public Student02(String name) {
this.name = name;
}
}
静态代码块
- 格式:
static{}
- 特点:需要通过static关键字修饰,随着类的加载而加载,并且自动触发、只执行一次
- 使用场景:在类加载的时候,做一些数据的初始化的时候使用。
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student("张三", 23);
}
}
class Student {
private String name;
private int age;
// 静态代码块
// 执行时机,随着类的加载而加载,而且执行一次
static {
System.out.println("静态代码块执行了。");
}
public Student() {
System.out.println("空参构造方法");
}
public Student(String name, int age) {
System.out.println("含参构造方法");
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;
}
}
这段代码的执行结果是
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=61260:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" mycodeblock.a01codeblockdemo01.Test
静态代码块执行了。
空参构造方法
含参构造方法
Process finished with exit code 0
静态代码块是很有用处的,用于数据初始化。
我想用一个集合去存放学生数据,然后打印。如果按照原有的思想去写这个代码
package mycodeblock.a02codeblockdemo02;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("hhh001", "张三", 24));
list.add(new Student("hhh002", "李四", 20));
list.add(new Student("hhh003", "王五", 22));
for (int i = 0; i < list.size(); i++) {
Student s = list.get(i);
System.out.println(s.getId() + ", " + s.getName() + ", " + s.getAge());
}
}
}
这么写,程序是可以实现的,但是有个弊端。main方法也是可以被调用的。还可以反复调用。
package mycodeblock.a02codeblockdemo02;
public class Test2 {
public static void main(String[] args) {
Test.main(null);
Test.main(null);
Test.main(null);
Test.main(null);
Test.main(null);
Test.main(null);
}
}
如果是这样的话,程序反复执行,那么集合和Student也会反复创建、
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("hhh001", "张三", 24));
list.add(new Student("hhh002", "李四", 20));
list.add(new Student("hhh003", "王五", 22));
这样是很占用内存的,因此用到了静态代码块。我直接写成完整代码
package mycodeblock.a02codeblockdemo02;
import java.util.ArrayList;
public class Test {
static ArrayList<Student> list = new ArrayList<>();
static {
list.add(new Student("hhh001", "张三", 24));
list.add(new Student("hhh002", "李四", 20));
list.add(new Student("hhh003", "王五", 22));
}
public static void main(String[] args) {
for (int i = 0; i < list.size(); i++) {
Student s = list.get(i);
System.out.println(s.getId() + ", " + s.getName() + ", " + s.getAge());
}
}
}
class Student {
private String id;
private String name;
private int age;
public Student() {
}
public Student(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
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 int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
再去看这个测试类里面的代码。把添加学生到集合的代码写到了代码块中,静态代码块随着类加载而加载,只执行一次,这个时候即使你反复调用也不会占用内存了。
还有上次写了一个学生管理系统。因为没有连接数据库,所以每一次执行程序都要你重新注册账户然后登录。这样很是麻烦。一开始我是聪明的,我直接写死了有个数据。用于登录。
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
ArrayList<User> userList = new ArrayList<>();
User u = new User("admin", "Admin00000", "11122223333", "11122233334455667x");
userList.add(u);
...
}
但是这种弊端,反复调用,内存占用。现在数据量不算多,如果说是占用,并不会占用多少、
这里就可以用静态代码块解决。在加载类的时候,就初始化一些必要的数据。
public class AppItheima {
static ArrayList<User> userList = new ArrayList<>();
static {
User u = new User("admin", "Admin00000", "11122223333", "11122233334455667x");
userList.add(u);
}
public static void main(String[] args) {
...
}
}
三种代码块中,局部代码块和构造代码块已经是out了,不是很常用。静态代码块是一个好东西。用于初始化。
抽象
还是用学生和老师举例子。学生和老师都继承于人。学生和老师的工作是不一样的,因此work方法需要被重写。
现在我在学生类里面忘记重写work方法或者直接忽略重写。代码是跑的通的。但是不符合我的设想。
因此在父类中把work定义成抽象方法。凡是被定义成抽象方法的方法都要被重写。否则报错;
在一个类中,无论有一个或者多个抽象方法,那么这个类要被定义成抽象类、
抽象类和抽象方法的定义格式
抽象方法的定义格式:
public abstract 返回值类型 方法名(列表参数);
抽象类的定义格式
public abstract clss 类名{}
package myabstract.a01abstractdemo1;
public abstract class Person {
public abstract void work();
}
以上就是一个简单的抽象类和抽象方法的定义格式
抽象类和抽象方法的注意事项
抽象类不能实例化
抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类
可以有构造方法
抽象类的子类
要么重写抽象类中的所有抽象方法
要么是抽象类
这些规则不需要死记硬背,现在的idea功能已经很强大了,你犯错了直接会报错
package myabstract.a01abstractdemo1;
public class Test {
public static void main(String[] args) {
Person p = new Person();
}
}
这段代码idea直接在new Person()
处标红了。也可以运行,直接报错
E:\yang\专业\Java Code\basic-code\day13\src\myabstract\a01abstractdemo1\Test.java:5:20
java: myabstract.a01abstractdemo1.Person是抽象的; 无法实例化
抽象类不可以实例化,也就是创建对象
package myabstract.a01abstractdemo1;
public abstract class Person {
// public abstract void work();
public void eat(){
System.out.println("正在睡觉");
}
}
这种情况是允许的,是抽象类,但是没有抽象方法。
package myabstract.a01abstractdemo1;
public class Person {
public abstract void work();
}
idea会在abstract处标红。在测试类中创建对象会报错
E:\yang\专业\Java Code\basic-code\day13\src\myabstract\a01abstractdemo1\Person.java:3:8
java: myabstract.a01abstractdemo1.Person不是抽象的, 并且未覆盖myabstract.a01abstractdemo1.Person中的抽象方法work()
一下演示一个正确的代码
package myabstract.a01abstractdemo1;
public class Test {
public static void main(String[] args) {
Student s = new Student("张三", 23);
s.work();
}
}
abstract class Person {
private String name;
private int age;
public Person() {
}
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 work();
}
class Student extends Person{
public Student() {
}
public Student(String name, int age) {
super(name, age);
}
@Override
public void work() {
System.out.println("学生正在学习");
}
}
抽象类不能创建对象,但是有构造方法是为什么
public Student(String name, int age) {
super(name, age);
}
在子类编写构造方法代码的时候,需要用到父类的构造方法super(name, age)
。父类没有会报错的。
抽象类的子类常规情况是重写抽象类的方法,而不是让子类也是抽象类。
编写带有抽象类的标准Javabean类
青蛙frog ;属性:名字,年龄;行为:吃虫子,喝水
狗Dog;属性:名字,年龄;行为:吃骨头,喝水
山羊Sheep;属性:名字,年龄;行为:吃草,喝水
package myabstract.a02abstractdemo2;
public class Test {
public static void main(String[] args) {
Frog f = new Frog("小绿", 1);
System.out.println(f.getName() + ", " + f.getAge());
f.eat();
f.drink();
}
}
abstract class Animal {
private String name;
private int age;
public Animal() {
}
public Animal(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 void drink(){
System.out.println("动物在喝水");
}
public abstract void eat();
}
class Dog extends Animal{
public Dog() {
}
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println("狗在吃骨头");
}
}
class Frog extends Animal{
public Frog() {
}
public Frog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println("青蛙在吃虫子");
}
}
class Sheep extends Animal{
public Sheep() {
}
public Sheep(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println("山羊在吃草");
}
}
接口
接口的介绍、引入
还是上面的例子,现在我想加入一个游泳的方法。狗和青蛙会游泳,但是兔子不会。因此游泳不可能写在父类了,兔子继承了那岂不是出bug了。
在狗和青蛙类里面定义游泳的方法,又出现了分支。swim和swimming都是游泳的意思,一个人开发能做到统一,但是一个团队呢。就要制定规则。而且排除这个例子,很多动物会游泳,很多不会。难不成复制和不复制?这就是接口解决的事情。
因此把游泳这个功能定义成一个接口,抽象的方法。做到了统一。
public abstract void swim();
接口:就是一种规则
老师是这么举例子的,比如说搬家,搬东西。一开始可以定义成一个方法。
public void 搬家(Car c){
}
只要把Car的对象传过去,那就可以实现搬家。
但是搬家并不是只能用车搬家。我可以找搬家公司,我可以用三轮车。我也可以肩扛。
问题来了,搬家公司、三轮车、肩扛都不会继承于Car类。那这个方法怎么用呢、
我现在需要什么,我不需要一个完美的继承体系,而是能搬家就行,完成这个工作。怎么干我不管,我只在乎把这件事完成。
因此可以定义一个运输的接口
public interface 运输 {
...
}
然后在搬家的方法中直接写接口的类型
public void 搬家(运输的接口 c){
}
因此调用方法的时候搬家(车)
还是搬家(搬家公司)
都可以实现
接口不代表一类事物,而是一种规则,是对行为的抽象
接口和抽象类的区别。接口也是抽象方法。
类是一类事物,接口是一种规则。抽象类作为父类,是有继承的。如果把游泳的方法定义在抽象类里面,那么兔子会继承,以后猫也会继承。鸟也会继承。但是他们不会游泳的。不符合现实。
定义成接口。青蛙、狗,以后还会有鱼等等都会游泳,那我就直接用游泳的这个规则。而且学生、老师是不是都会游泳。虽然人和动物是两个不相关的类。但是接口,规则。人暗战这个规则就行了。
接口的定义和使用
接口用关键字interface来定义
public interface 接口名 {}
接口不能实例化
接口和类之间是实现关系,通过implements关键字表示
public class 类名 implements 接口名 {}
接口的子类(实现类)
要么重写接口中的所有抽象方法
要么是抽象类
注意1:接口和类的实现关系,可以单实现,也可以多实现。
public class 类名 implements 接口名1,接口名2 {}
注意2:实现类还可以在继承一个类的同时实现多个接口。
public class 类名 extends 父类 implements 接口名1,接口名2 {}
老师的ppt,一大堆好复杂,接口的定义public interface 接口名 {}
,实现这个接public class 类名 implements 接口名 {}
,一个类可以实现多个接口public class 类名 implements 接口名1,接口名2 {}
,还有更复杂的public class 类名 extends 父类 implements 接口名1,接口名2 {}
接下来把上面的例子做一下。熟悉一个接口的语法
public class Test {
public static void main(String[] args) {
Frog f = new Frog("小绿", 1);
System.out.println(f.getName() + ", " + f.getAge());
f.eat();
f.drink();
f.siwm();
Sheep s = new Sheep("小羊", 2);
System.out.println(s.getName() + ", " + s.getAge());
s.eat();
s.drink();
}
}
interface Siwm {
public abstract void siwm();
}
abstract class Animal {
private String name;
private int age;
public Animal() {
}
public Animal(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 void drink(){
System.out.println("动物在喝水");
}
public abstract void eat();
}
class Dog extends Animal implements Siwm{
public Dog() {
}
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println("狗在吃骨头");
}
@Override
public void siwm() {
System.out.println("狗在游泳");
}
}
class Frog extends Animal implements Siwm{
public Frog() {
}
public Frog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println("青蛙在吃虫子");
}
@Override
public void siwm() {
System.out.println("青蛙在游泳");
}
}
class Sheep extends Animal {
public Sheep() {
}
public Sheep(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println("山羊在吃草");
}
}
代码的逻辑很简单。略过了
接口中的成员特点
成员变量
只能是常量
默认修饰符:
public static final
多个子类的共有属性会抽取到父类。因此在接口中不会出现name,age等变量。而且接口属于一种规则,规则不能发生改变。
接口里面的变量都是常量,用final修饰。不能改变。static是方便通过类名.成员变量
的方式调用。
默认的,那就是即使不写,Java也会这么规定。
成员方法
只能是抽象方法
默认修饰符:
public abstract
构造方法
这里老师用Java内存分析工具做的,我果断放弃了。
编写带有接口和抽象类的标准Javabean类
我们现在有乒乓球运动员和篮球运动员,乒乓球教练和篮球教练。为了出国交流,跟乒乓球相关的人员都需要学习英语。请用所有知识分析,在这个案例中,哪些是具体类,哪些是抽象类,哪些是接口?
- 乒乓球运动员:姓名,年龄,学打乒乓球,说英语
- 篮球运动员:姓名,年龄,学打篮球
- 乒乓球教练:姓名,年龄,教打乒乓球,说英语
- 篮球教练:姓名,年龄,教打篮球
public class Test {
public static void main(String[] args) {
PingPongSportsman pps = new PingPongSportsman("张三", 22);
System.out.println(pps.getName() + ", " + pps.getAge());
pps.study();
pps.speakEnglish();
BasketballCoach bc = new BasketballCoach("老张", 46);
System.out.println(bc.getName() + ", " + bc.getAge());
bc.teach();
}
}
// 不希望人被实例化,设置为抽象类
abstract class Person {
private String name;
private int age;
public Person() {
}
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;
}
}
interface English {
public abstract void speakEnglish();
}
abstract class Coach extends Person{
public Coach() {
}
public Coach(String name, int age) {
super(name, age);
}
public abstract void teach();
}
abstract class Sportsman extends Person {
public Sportsman() {
}
public Sportsman(String name, int age) {
super(name, age);
}
public abstract void study();
}
class PingPongSportsman extends Sportsman implements English{
public PingPongSportsman() {
}
public PingPongSportsman(String name, int age) {
super(name, age);
}
@Override
public void speakEnglish() {
System.out.println("乒乓球运动员" + super.getName() + "正在说英语");
}
@Override
public void study() {
System.out.println("乒乓球运动员" + super.getName() + "正在练习打乒乓球");
}
}
class BasketballSportsman extends Sportsman{
public BasketballSportsman() {
}
public BasketballSportsman(String name, int age) {
super(name, age);
}
@Override
public void study() {
System.out.println("篮球运动员" + super.getName() + "正在练习打篮球");
}
}
class BasketballCoach extends Coach{
public BasketballCoach() {
}
public BasketballCoach(String name, int age) {
super(name, age);
}
@Override
public void teach() {
System.out.println("篮球教练" + super.getName() + "正在教运动员打篮球");
}
}
class PingPongCoach extends Coach implements English{
public PingPongCoach() {
}
public PingPongCoach(String name, int age) {
super(name, age);
}
@Override
public void teach() {
System.out.println("乒乓球教练" + super.getName() + "正在教运动员打乒乓球");
}
@Override
public void speakEnglish() {
System.out.println("乒乓球教练" + super.getName() + "正在说英语");
}
}
代码挺长的,但是逻辑很基础。
多学三招
JDK8开始接口新增的方法
- JDK7以前:接口中只能定义抽象方法。
- JDK8的新特性:接口中可以定义有方法体的方法。(默认、静态)
- JDK9的新特性:接口中可以定义私有方法
光看这些特性,我也不懂。
有这么一个场景。定义了一个接口。
interface Inter {
public abstract void method();
}
然后去实现。也实现了。
class InterImpl implements Inter{
public void method(){
...
}
}
但是过几天,我又在Inter接口里面添加了一个新的规则、那么InterImpl瞬间报错。因为每个抽象方法都要被重写。如果不重写,会报错。
如何避免这个问题了,那就是定义有方法体的方法,就像JDK8和JDK9描述
interface Inter {
public abstract void method();
定义有方法体的方法
}
接口中有方法体的方法是在接口升级时为了兼容性而使用的
接口升级,做项目会有版本,1.0,2.0,可能你会在1.0的时候定义了一个接口,当然也实现了。然后在2.0时候,你把这个接口丰富了一下,添加了10个方法。如果是之前,实现类会直接报错。但是有了JDK8和JDK9的新特性。那么就不会报错了
此时实现类就不需要立马修改了,等以后用到某个规则了,再重写就行了
在JDK8以后接口中新增的默认方法
允许在接口中定义默认方法,需要使用关键字default修饰
作用:解决接口升级的问题
接口中默认方法的定义格式:
- 格式:
public default 返回值类型 方法名(参数列表){}
- 范例:
public default void show() {}
接口中默认方法的注意事项:
- 默认方法不是抽象方法,所以不强制被重写。但是如果被重写,重写的时候去掉default关键字
- public可以省略,default:不能省略
- 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写
package myinterface.a03myinterfacedemo3;
public interface Inter {
// 默认方法
public default void show(){
System.out.println("接口中的默认方法show()");
}
// 抽象方法
public abstract void method();
}
以上代码定义了一个默认方法和抽象方法。注意default和abstract的用法。接着去实现这个接口,并去创建对象
package myinterface.a03myinterfacedemo3;
public class Test {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.method();
ii.show();
}
}
class InterImpl implements Inter {
@Override
public void method() {
System.out.println("实现类重写了抽象方法");
}
}
在之前,代码肯定是有问题的,因为你要重写接口里的全部方法,但是这一次代码可以运行
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=56549:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" myinterface.a03myinterfacedemo3.Test
实现类重写了抽象方法
接口中的默认方法show()
Process finished with exit code 0
也可以直接重写这个默认方法
package myinterface.a03myinterfacedemo3;
public class Test {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.method();
ii.show();
}
}
class InterImpl implements Inter {
@Override
public void method() {
System.out.println("实现类重写了抽象方法");
}
@Override
public void show() {
System.out.println("实现类重写了默认方法");
}
}
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=56583:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" myinterface.a03myinterfacedemo3.Test
实现类重写了抽象方法
实现类重写了默认方法
Process finished with exit code 0
在JDK8以后接口中新增的静态方法
接口中静态方法的定义格式:
- 格式:
public static 返回值类型 方法名(参数列表){}
- 范例:
public static void show(){}
接口中静态方法的注意事项:
- 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用;
- public可以省略,static不能省略
在接口中定义一个静态方法
package myinterface.a04myinterfacedemo4;
public interface Inter {
// 抽象方法
public abstract void method();
// 静态方法
public static void show(){
System.out.println("接口中的静态方法show()");
}
}
还是去实现这个接口去看怎么使用。
package myinterface.a04myinterfacedemo4;
public class Test {
public static void main(String[] args) {
// 调用接口中的静态方法
Inter.show(); // 接口中的静态方法show()
}
}
因为接口中的静态方法不能通过实现类去重写,也不能通过实现类去调用,因此完全不用写实现类,直接在测试类中接口名.静态方法()
的形式使用
在JDK9以后接口中新增的私有方法
这种方法是不允许外界访问的。
接口中私有方法的定义格式:
- 格式1:
private 返回值类型 方法名 (参数列表) {}
范例1:
private void show() {}
- 格式2:
private static 返回值类型 方法名 (参数列表) {}
范例1:
private static void show() {}
普通私有方法的使用方式
package myinterface.a05myinterfacedemo5;
public interface Inter {
public default void method1(){
System.out.println("method1()方法开始执行了");
log();
}
// 普通的私有方法,给默认方法服务
private void log(){
System.out.println("正在监控程序运行状态,这里有100行代码");
}
}
静态的私有方法
package myinterface.a05myinterfacedemo5;
public interface Inter {
public static void method1(){
System.out.println("method1()方法开始执行了");
log();
}
// 静态的私有方法,给静态方法服务
private static void log(){
System.out.println("正在监控程序运行状态,这里有100行代码");
}
}
通过代码,记住两句话就好。也写在注释里了
普通的私有方法,给默认方法服务
静态的私有方法,给静态方法服务
混用是不行的,idea会报错。标红
接口的应用
- 接口代表规则,是行为的抽象。想要让哪个类拥有一个行为,就让这个类实现对应的接口就可以了。
- 当一个方法的参数是接口时,可以传递接口所有实现类的对象,这种方式称之为接口多态。
第一种,在接口的引入和介绍中已经了解到了。第二种,接口的多态第一次听说
package myinterface.a06myinterfacedemo6;
public class Test {
public static void main(String[] args) {
// 使用接口类型的引用指向不同的实现类对象
Animal myDog = new Dog();
Animal myCat = new Cat();
// 调用makeSound方法,多态性使得我们可以使用同一个接口引用调用不同实现类的实现
myDog.makeSound(); // 输出: Woof!
myCat.makeSound(); // 输出: Meow!
// Animal mySheep = new Sheep(); // java: 不兼容的类型:
}
}
// 定义一个接口
interface Animal {
void makeSound();
}
// 定义两个实现类
class Dog implements Animal {
public void makeSound() {
System.out.println("Woof!");
}
}
class Cat implements Animal {
public void makeSound() {
System.out.println("Meow!");
}
}
class Sheep{
public void makeSound() {
System.out.println("baa!");
}
}
在这个例子中,定义了一个名为Animal
的接口,它有一个makeSound
方法。然后,创建了两个实现Animal
接口的实现类:Dog
和Cat
。在Test
方法中,使用接口 对象名 = new 实现对象()
的方式创建了Dog
和Cat
对象。当我们调用myAnimal.makeSound()
时,会根据实际指向的对象类型执行相应的方法。这就是接口多态的体现。
执行 Animal mySheep = new Sheep();
会报错,因为Sheep没有实现Animal的接口
E:\yang\专业\Java Code\basic-code\day13\src\myinterface\a06myinterfacedemo6\Test.java:13:26
java: 不兼容的类型: myinterface.a06myinterfacedemo6.Sheep无法转换为myinterface.a06myinterfacedemo6.Animal
适配器设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
简单理解:设计模式就是各种套路。
设计模式在编程界很常听到。按我现在的理解就是一种代码规范。
- 当一个接口中抽象方法过多,但是我只要使用其中一部分的时候,就可以用适配器设计模式
- 书写步骤:
- 编写中间类XXXAdapter,实现对应的接口;
- 对接口中的抽象方法进行空实现
- 让真正的实现类继承中间类,并重写需要用的方法
- 为了避免其他类创建适配器类的对象,中间的适配器类用abstract进行修饰
假设我现在写了一个接口,里面有5个抽象方法
package myinterface.a07myinterfacedemo7;
public interface Inter {
public abstract void method1();
public abstract void method2();
public abstract void method3();
public abstract void method4();
public abstract void method5();
}
接下来就是实现接口。但是我只想用第三个抽象方法method3()
。
实现接口,按照前面学到的,要重写所以的抽象方法。
package myinterface.a07myinterfacedemo7;
public class InterImpl implements Inter{
@Override
public void method1() {
}
@Override
public void method2() {
}
@Override
public void method3() {
}
@Override
public void method4() {
}
@Override
public void method5() {
}
}
我的初衷是我想实现接口里的method3
方法。虽然可以对其他方法进行空实现。这种方法好是好,但却影响代码阅读性。
这个时候用到了适配器设计模式。在接口文件Inter
和实现类文件InterImpl
的中间编写一个类InterAdapter
让这个类去实现接口
package myinterface.a07myinterfacedemo7;
public abstract class InterAdapter implements Inter{
@Override
public void method1() {
}
@Override
public void method2() {
}
@Override
public void method3() {
}
@Override
public void method4() {
}
@Override
public void method5() {
}
}
还是空实现。因为外界创建这个InterAdapter
类是没有意义的,所以就可以用abstract
来修饰,
接下来,让InterImpl去继承InterAdapter。在这个类里面,我需要用到什么方法,就去重写什么方法。
package myinterface.a07myinterfacedemo7;
public class InterImpl extends InterAdapter{
@Override
public void method3() {
System.out.println("我需要使用method3方法");
}
}
然后再写个测试类,去使用。
package myinterface.a07myinterfacedemo7;
public class Test {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.method3(); // 我需要使用method3方法
}
}
对于这个设计模式。主要是提高了代码的阅读性。如果把空实现和实现都写到一起。代码不报错。但是影响代码阅读性。
内部类
内部类介绍
内部类是类的五大成员之一,而类的五大成员除了内部类,其他都学过,有属性、方法、构造方法和代码块。
内部类,在类的里面在定义一个类。
public class Outer {
public class Inner {
}
}
这里又学到了一个名词,外部其他类。外部其他类就是之前写过的测试类、JavaBean类以及实现类。
Outer是外部类,Inner是内部类。
有这么一个需求,定义一个JavaBean类用于类描述汽车。其中属性:汽车的品牌,车龄,颜色,发动机的品牌,使用年限。
这个很简单啊
public class Car{
String carName;
int carAge;
int carColor;
String engineName;
int engineAge;
}
根据以前的知识,咱们都会这么写。
面向对象是对实现世界的建模,发动机是一个个体,与车并没有很大的关系。那就把发动机单独写成一个类。可单独一个发动机并没有什么意义。因此最好是把发动机定义成内部类。汽车类的里面。
- 内部类表示的事物是外部类的一部分
- 内部类单独出现没有任何意义
内部类的访问特点
- 内部类可以直接访问外部类的成员,包括私有
- 外部类要访问内部类的成员,必须创建对象
接下来,定义一个内部类
package myinnerclass.a01innerclassdemo1;
public class Car {
String CarName;
int CarAge;
String CarColor;
class Engine {
String engineName;
int engineAge;
}
}
上面是内部类的定义方式
package myinnerclass.a01innerclassdemo1;
public class Car {
private String carName;
int CarAge;
String CarColor;
class Engine {
String engineName;
int engineAge;
public void show(){
System.out.println(engineName);
System.out.println(carName);
}
}
}
内部类可以直接访问外部类的成员,即使是私有。
package myinnerclass.a01innerclassdemo1;
public class Car {
private String carName;
int CarAge;
String CarColor;
public void show(){
Engine e = new Engine(); // 创建内部类对象
System.out.println(this.carName);
System.out.println(e.engineName);
}
class Engine {
String engineName;
int engineAge;
public void show(){
System.out.println(engineName);
System.out.println(carName);
}
}
}
外部类访问内部类必须创建内部类对象,否则idea会标红,也会报错
E:\yang\专业\Java Code\basic-code\day13\src\myinnerclass\a01innerclassdemo1\Car.java:11:28
java: 找不到符号
符号: 变量 engineName
位置: 类 myinnerclass.a01innerclassdemo1.Car
因为成员变量engineName不在Car里面,this.engineName
是找不到的
内部类的应用,老师带我们看了一段关于ArrayList的源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
...
}
...
}
Itr就是定义在ArrayList里面的内部类。用于对数组的迭代。遍历数组。
Itr类不能单独出现,因为没有集合,也就不可能遍历集合。因此Java就是这么设计的。
有两个事物,A和B,事物B是事物A的一部分,而且事物B单独存在没有意义,那么就要把事物A定义成外部类,把事物B定义成内部类
内部类的分类
成员内部类
写在成员的位置,属于外部类的成员
成员内部类可以被一些修饰符所修饰,比如:private、默认、protected、public、static等
在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。
package myinnerclass.a01innerclassdemo1;
public class Car {
String CarName;
int CarAge;
String CarColor;
class Engine {
String engineName;
int engineAge;
}
}
刚刚写的代码,就是一个标准的成员内部类。
获取成员内部类对象的两种方式
package myinnerclass.a02innerclassdemo2;
public class Outer {
class Inner{
}
}
方法一:直接创建格式:
外部类名.内部类名 对象名 = 外部类对象.内部类对象;
package myinnerclass.a02innerclassdemo2;
public class Test {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
}
}
这个就相当于链式调用。
如果成员内部类被private修饰,那么就不能被外界访问。这个时候就用到了方法二
package myinnerclass.a02innerclassdemo2;
public class Outer {
private class Inner{
}
public Inner getInstance(){
return new Inner();
}
}
方法二:外部类编写方法,对外提供内部类对象
package myinnerclass.a02innerclassdemo2;
public class Test {
public static void main(String[] args) {
// Outer.Inner oi = new Outer().new Inner();
Outer outer = new Outer();
Object inner = outer.getInstance();
System.out.println(outer.getInstance()); // myinnerclass.a02innerclassdemo2.Outer$Inner@41629346
}
}
这里是个小细节,通过外部类编写方法调用内部类对象时,outer.getInstance();
的类型就不是Outer.Inner
了、而是他的父类。因为Inner类没有继承关系,所以就用了默认父类Object
也可以直接使用 System.out.println(outer.getInstance());
在jdk16之前,在内部类中定义静态变量会报错。因为我现在是jdk17,我在jdk17中定义静态变量不会报错
package myinnerclass.a02innerclassdemo2;
public class Outer {
private class Inner{
static int a = 10;
}
}
在idea中可以切换jdk版本。
在菜单File中下面选择Project Structure,快捷键是Ctrl+Alt+Shift+S,在项目设置Project Settings中选择Project,右边
- SDK:表示当前项目安装的是哪个jdk;
- Language level:表示用哪个版本编译项目。这个选择必须要小于或者等于安装的jdk版本,默认选择是SDK default,与安装版本一致。
修改jdk版本,也就是修改Language level。上面说16开始才可以定义静态变量,那我改成15
idea会在static出报红,提示信息:Static declarations in inner classes are not supported at language level ‘15’
有一个关于内部类的面试题
package myinnerclass.a03innerclassdemo3;
import java.util.PropertyResourceBundle;
public class Outer {
int a = 10;
class Inner{
private int a = 30;
private void show(){
int a = 30;
// System.out.println(???); // 10
// System.out.println(???); // 20
// System.out.println(???); // 30
}
}
public Inner getInstance(){
return new Inner();
}
}
问号里面应该怎么写
难点在于方法里的成员变量、内部类成员变量和外部类成员变量都重名了,如果不重名,
内部类可以直接访问外部类的成员,包括私有
System.out.println(Outer.this.a); // 10
System.out.println(this.a); // 20
System.out.println(a); // 30
体现了一点就近原则
看一下完整的代码
package myinnerclass.a03innerclassdemo3;
public class Test {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
oi.show();
}
}
class Outer {
int a = 10;
class Inner{
private int a = 30;
void show(){
int a = 30;
System.out.println(Outer.this.a); // 10
System.out.println(this.a); // 20
System.out.println(a); // 30
}
}
public Inner getInstance(){
return new Inner();
}
}
通过内存图分析这段代码,
线有点乱,不过能看
- 外部类和内部类在内存中是两个独立的字节码文件。这个结论可以在本地文件中验证,打开项目的根目录,会有一个out目录,慢慢找会发现有两个文件分别是外部类字节码文件
Outer.class
和内部类字节码文件Outer$Inner.class
- 在堆内存中会开辟外部类和内部类的内存空间。而且虚拟机会给Imner的内存对象添加一个隐藏的成员变量Outer.this,用于记录外部类对象的地址值。
Outer.Inner oi = new Outer().new Inner();
中的oi
记录的是内部类的地址值002oi.shou()
的调用者是002,sout(a)
用的是就近原则,是方法里面的变量a,sout(this.a)
是对象002的变量a,而Outer.this.a
,因为Outer.this
的值是001,所以找地址值是001的变量a
静态内部类
静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的需要创建对象。
package myinnerclass.a01innerclassdemo1;
public class Car {
String CarName;
int CarAge;
String CarColor;
static class Engine {
String engineName;
int engineAge;
}
}
静态内部类就是在成员内部类的基础上加了一个static修饰。循环咱们之前学习过的一个原则是静态只能访问静态。
- 创建静态内部类对象的格式:
外部类名.内部类名 对象名 = new 外部类名.内部类名();
- 调用非静态方法的格式:先创建对象,用对象调用
- 调用静态方法的格式:
外部类名.内部类名.方法名():
package myinnerclass.a04innerclassdemo4;
public class Outer {
int a = 10;
static int b = 20;
static class Inner{
public void show1(){
System.out.println(a); // 标红
System.out.println(b);
}
static public void show2(){
System.out.println(a); // 标红
System.out.println(b);
}
}
}
只能访问被static修饰的外部类成员变量
package myinnerclass.a04innerclassdemo4;
public class Outer {
int a = 10;
static int b = 20;
static class Inner{
public void show1(){
// 创建对象访问外部类变量
Outer outer = new Outer();
System.out.println(outer.a);
System.out.println(b);
}
static public void show2(){
// 创建对象访问外部类变量
Outer outer = new Outer();
System.out.println(outer.a);
System.out.println(b);
}
}
}
需要在内部类中创建外部类的变量,才能访问非静态内容
package myinnerclass.a04innerclassdemo4;
public class Test {
public static void main(String[] args) {
// 创建静态内部类对象
Outer.Inner oi = new Outer.Inner();
// 访问非静态方法
oi.show1(); // 非静态方法被调用了
// 访问静态方法
// oi.show2(); // 静态方法被调用了
Outer.Inner.show2(); // 静态方法被调用了
}
}
class Outer {
static class Inner{
public void show1(){
System.out.println("非静态方法被调用了");
}
static public void show2(){
System.out.println("静态方法被调用了");
}
}
}
创建静态内部类,以及调用方法、调用非静态方法必须是对象名.方法名
,而调用静态方法有两种方式:对象名.方法名
和外部类名.内部类名.方法名():
,但是强推外部类名.内部类名.方法名():
局部内部类
- 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量。
- 外界是无法直接使用,需要在方法内部创建对象并使用。
- 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。
package myinnerclass.a05innerclassdemo5;
public class Outer {
public void show(){
class Inner{
}
}
}
局部内部类的定义方式,局部内部类的地位和方法里局部变量一样。能修饰局部变量的关键字同样也可以修饰局部内部类。
package myinnerclass.a05innerclassdemo5;
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
outer.show();
}
}
class Outer {
int a = 10;
public void show(){
int b = 20;
class Inner{
String name;
int age;
public void method1(){
System.out.println(a);
System.out.println(b);
System.out.println("非静态方法method1");
}
public static void method2(){
System.out.println("静态方法method2");
}
}
// 创建局部内部类对象
Inner inner = new Inner();
System.out.println(inner.name);
System.out.println(inner.age);
inner.method1();
Inner.method2();
}
}
这个的运行结果是
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=52143:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" myinnerclass.a05innerclassdemo5.Test
null
0
10
20
非静态方法method1
静态方法method2
这个用的不多,了解一下即可。注意局部内部类不能在测试类里面创建,
匿名内部类
匿名,那就是没有名字。没有名字的内部类。格式如下
new 类名或者接口名(){
重写方法;
};
举例:
package myinnerclass.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
new Swim(){
@Override
public void swim() {
System.out.println("重写了这个方法");
}
};
}
}
interface Swim {
public abstract void swim();
}
在测试类中创建了一个匿名内部类。有没有想过,匿名内部类是那一部分。换句话说,这段代码能拆解、
// 没有名字的类
{
@Override
public void swim() {
System.out.println("重写了这个方法");
}
};
上面就是没有名字的类,匿名类。匿名类实现了Swim()
接口、实现接口,那就要在重写方法,方法写在了匿名类中。new
的是没有名字的类,匿名类。
package myinnerclass.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
new Animal(){
@Override
public void eat() {
System.out.println("重写eat方法");
}
};
}
}
abstract class Animal {
public abstract void eat();
}
上面的Swim是实现,这里的Animal是继承。其他都是一样的。
匿名类虽然没有名称,但是会在本地目录产生字节码文件。因为我创建了两个匿名类,所以会在本地out目录下产生两个匿名类的字节码文件Test$1.class
和Test$2.class
。可以得出虚拟机会对匿名类进行命名外部类名$序号
这里多学一招,反编译。把字节码文件变成我们能看懂的文件
在当前目录下打开cmd命令行工具,Javap
进行反编译
PS ...\yinnerclass\a06innerclassdemo6> javap '.\Test$1.class'
Compiled from "Test.java"
class myinnerclass.a06innerclassdemo6.Test$1 implements myinnerclass.a06innerclassdemo6.Swim {
myinnerclass.a06innerclassdemo6.Test$1();
public void swim();
}
javap '.\Test$1.class'
是对Test$1.class
进行反编译
class myinnerclass.a06innerclassdemo6.Test$1 implements myinnerclass.a06innerclassdemo6.Swim
中class myinnerclass.a06innerclassdemo6.Test$1
是类名,implements myinnerclass.a06innerclassdemo6.Swim
实现这个接口
myinnerclass.a06innerclassdemo6.Test$1();
是Java虚拟机提供的默认无参构造。public void swim();
是Swim接口里的方法
package myinnerclass.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
// 以前写法
// Dog d = new Dog();
// method(d);
// 匿名内部类的用法
method(new Animal(){
@Override
public void eat() {
System.out.println("重写eat方法");
}
}); // 重写eat方法
}
public static void method(Animal a){ // 相当于Animal a = 子类对象
a.eat();
}
}
abstract class Animal {
public abstract void eat();
}
这么写的好处是Dog类我只用一次,就不能创建Dog的Java文件了、
package myinnerclass.a06innerclassdemo6;
public class Test2 {
public static void main(String[] args) {
// 接口的多态
Swim s = new Swim() {
@Override
public void swim() {
System.out.println("重写游泳方法");
}
};
s.swim(); // 重写游泳方法
}
}
解锁高阶玩法,这里就是接口的多态
package myinnerclass.a06innerclassdemo6;
public class Test2 {
public static void main(String[] args) {
new Swim(){
@Override
public void swim() {
System.out.println("重写swim方法");
}
}.swim(); // 重写swim方法
}
}
这里相当于自己调用自己。
这部分内容更像是链式编程。
总结
这部分学来好多。一边学一边忘。
学完这些。面向对象的思想应该也就是学到了。
谢谢大家阅读。有问题欢迎指正。
ss Test {
public static void main(String[] args) {
// 创建静态内部类对象
Outer.Inner oi = new Outer.Inner();
// 访问非静态方法
oi.show1(); // 非静态方法被调用了
// 访问静态方法
// oi.show2(); // 静态方法被调用了
Outer.Inner.show2(); // 静态方法被调用了
}
}
class Outer {
static class Inner{
public void show1(){
System.out.println("非静态方法被调用了");
}
static public void show2(){
System.out.println("静态方法被调用了");
}
}
}
创建静态内部类,以及调用方法、调用非静态方法必须是`对象名.方法名`,而调用静态方法有两种方式:`对象名.方法名`和`外部类名.内部类名.方法名():`,但是强推`外部类名.内部类名.方法名():`
### 局部内部类
> * 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量。
> * 外界是无法直接使用,需要在方法内部创建对象并使用。
> * 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。
```java
package myinnerclass.a05innerclassdemo5;
public class Outer {
public void show(){
class Inner{
}
}
}
局部内部类的定义方式,局部内部类的地位和方法里局部变量一样。能修饰局部变量的关键字同样也可以修饰局部内部类。
package myinnerclass.a05innerclassdemo5;
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
outer.show();
}
}
class Outer {
int a = 10;
public void show(){
int b = 20;
class Inner{
String name;
int age;
public void method1(){
System.out.println(a);
System.out.println(b);
System.out.println("非静态方法method1");
}
public static void method2(){
System.out.println("静态方法method2");
}
}
// 创建局部内部类对象
Inner inner = new Inner();
System.out.println(inner.name);
System.out.println(inner.age);
inner.method1();
Inner.method2();
}
}
这个的运行结果是
D:\Develop\Java\jdk-17\bin\java.exe "-javaagent:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\lib\idea_rt.jar=52143:D:\Develop\JetBrains\IntelliJ IDEA Community Edition 2024.1.2\bin" -Dfile.encoding=UTF-8 -classpath "E:\yang\专业\Java Code\basic-code\out\production\day13" myinnerclass.a05innerclassdemo5.Test
null
0
10
20
非静态方法method1
静态方法method2
这个用的不多,了解一下即可。注意局部内部类不能在测试类里面创建,
匿名内部类
匿名,那就是没有名字。没有名字的内部类。格式如下
new 类名或者接口名(){
重写方法;
};
举例:
package myinnerclass.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
new Swim(){
@Override
public void swim() {
System.out.println("重写了这个方法");
}
};
}
}
interface Swim {
public abstract void swim();
}
在测试类中创建了一个匿名内部类。有没有想过,匿名内部类是那一部分。换句话说,这段代码能拆解、
// 没有名字的类
{
@Override
public void swim() {
System.out.println("重写了这个方法");
}
};
上面就是没有名字的类,匿名类。匿名类实现了Swim()
接口、实现接口,那就要在重写方法,方法写在了匿名类中。new
的是没有名字的类,匿名类。
package myinnerclass.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
new Animal(){
@Override
public void eat() {
System.out.println("重写eat方法");
}
};
}
}
abstract class Animal {
public abstract void eat();
}
上面的Swim是实现,这里的Animal是继承。其他都是一样的。
匿名类虽然没有名称,但是会在本地目录产生字节码文件。因为我创建了两个匿名类,所以会在本地out目录下产生两个匿名类的字节码文件Test$1.class
和Test$2.class
。可以得出虚拟机会对匿名类进行命名外部类名$序号
这里多学一招,反编译。把字节码文件变成我们能看懂的文件
在当前目录下打开cmd命令行工具,Javap
进行反编译
PS ...\yinnerclass\a06innerclassdemo6> javap '.\Test$1.class'
Compiled from "Test.java"
class myinnerclass.a06innerclassdemo6.Test$1 implements myinnerclass.a06innerclassdemo6.Swim {
myinnerclass.a06innerclassdemo6.Test$1();
public void swim();
}
javap '.\Test$1.class'
是对Test$1.class
进行反编译
class myinnerclass.a06innerclassdemo6.Test$1 implements myinnerclass.a06innerclassdemo6.Swim
中class myinnerclass.a06innerclassdemo6.Test$1
是类名,implements myinnerclass.a06innerclassdemo6.Swim
实现这个接口
myinnerclass.a06innerclassdemo6.Test$1();
是Java虚拟机提供的默认无参构造。public void swim();
是Swim接口里的方法
package myinnerclass.a06innerclassdemo6;
public class Test {
public static void main(String[] args) {
// 以前写法
// Dog d = new Dog();
// method(d);
// 匿名内部类的用法
method(new Animal(){
@Override
public void eat() {
System.out.println("重写eat方法");
}
}); // 重写eat方法
}
public static void method(Animal a){ // 相当于Animal a = 子类对象
a.eat();
}
}
abstract class Animal {
public abstract void eat();
}
这么写的好处是Dog类我只用一次,就不能创建Dog的Java文件了、
package myinnerclass.a06innerclassdemo6;
public class Test2 {
public static void main(String[] args) {
// 接口的多态
Swim s = new Swim() {
@Override
public void swim() {
System.out.println("重写游泳方法");
}
};
s.swim(); // 重写游泳方法
}
}
解锁高阶玩法,这里就是接口的多态
package myinnerclass.a06innerclassdemo6;
public class Test2 {
public static void main(String[] args) {
new Swim(){
@Override
public void swim() {
System.out.println("重写swim方法");
}
}.swim(); // 重写swim方法
}
}
这里相当于自己调用自己。
这部分内容更像是链式编程。
总结
这部分学来好多。一边学一边忘。
学完这些。面向对象的思想应该也就是学到了。
谢谢大家阅读。有问题欢迎指正。