day3 成员,局部,匿名变量, 封装(构造,修饰符,this),继承super,多态,单例模式,抽象类,接口,(成员,局部[匿名]内部类)
Java 是面向对象的语言
对象:真实存在唯一的事物
类:具有相同的属性。对某种类型的事物的公共属性和行为的抽取
面向过程(C语言)侧重于过程
面向对象(java)侧重是对象,找合适的对象做合适的事情
1 SUN已经定义好了很多类,我们只需要认识这些类,通过类创建对象
2 自定义类,来创建对象使用
自定义类步骤:
- 自定义类 class Animal{}
- 通过类创建对象,类名 变量名 = new 类名() Animal cat = new Animal()
- 访问设置对象的属性或调用对象的功能
对象.属性名 对象.方法名()
class Car { //事物的公共属性,使用成员变量描述 String name, color; int wheel; //事物的公共行为,使用函数描述 public void run() { } }
像脚踏板就不能列入公共属性,因为不说所有的车都有脚踏板
生成了匿名对象,调用run方法
new Car().run();
也可以使用自定义类声明变量
Car car = new Car()来创建对象
成员属性出现在了堆内存中,会有一个默认的初始值
|
数据类型 |
默认初始值 |
|
int |
0 |
|
float |
0.0f |
|
double |
0.0 |
|
char |
‘’ |
|
String |
null |
|
引用数据类型 |
null |
对象一旦创建,对象的成员变量也会马上分配默认的初始值
通过new 类 对同一个类new了两个对象,这个时候两个对象的堆内存地址不同

public class Demo1 { public static void main(String[] args) { new Car().run(); } } class Car { //事物的公共属性,使用成员变量描述 String name, color; int wheel; //事物的公共行为,使用函数描述 public void run() { System.out.println("The car is running"); System.out.println(wheel);//变量在同一个作用域上,可以直接访问的 } }
初学者经典错误:
1 不在同一个范围内随意地引用了变量。变量在同一个作用域(大括号)上,是可以直接访问的
2 如果是一个类要访问另一个类的变量时,那么这时候就只能通过创建另一个类的对象进行访问(仅当前适用, 如果该属性是静态的static, 那么就可以用 类.属性 来调用)
class Car { //事物的公共属性,使用成员变量描述 static String name, color; int wheel; //事物的公共行为,使用函数描述 public void run() { int passengers; // 局部变量:函数内部定义 System.out.println("The car is running"); System.out.println(wheel);//变量在同一个作用域上,可以直接访问的 } }
成员变量与局部变量区别:
定义位置:
- 成员变量:类之内,方法之外
- 局部变量:方法之内
作用上的区别:
- 成员变量:描述一类事物的公共属性
- 局部变量:提供一个变量给方法内部使用
生命周期的区别:
- 成员变量:随着对象的创建而存在,随着对象的消失而消失。因为类的属性都是这一类对象的共有属性
- 局部变量:调用了相应的方法时,执行到了创建变量语句时才存在。一旦出了自己的方法的作用域马上从内存消失
初始值的区别:
- 成员变量:有默认初始值 (null 他们虽然是null但是每个都是自己的null,内存地址不同)
- 数据类型 初始值
Int 0
Float 0.0f
Double 0.0
局部变量:无默认初始值,必须先初始化才能使用。可以先声明再赋值,如果只声明了,别的方法要访问就会报错
匿名对象:没有名字的对象。没有引用类型变量指向的对象称匿名对象
Student stu = new Student() 使用Student类声明一个变量stu,指向了一个对象Student
new Student() //匿名对象,没有声明变量
注意:1 我们一般不会给匿名对象赋予属性值,因为永远无法获取到
new Student.name = “alex”;
System.out.println(new Student() == new Student()) 结果false, ==号用于引用类型变量时,比较的是内存地址,看看两个匿名对象是否是同一个。每new一个对象都会分配新的内存
2 两个匿名对象永远都不可能是同一个对象
匿名对象的应用场景:
1 如果需要调用一个类的方法,只调用1次时,而调用完这个方法后,该对象就不再使用了,这时可以用匿名对象。匿名对象使用后就消失,节约内存
new Student().study()
2 可以作为实参调用函数
CarFactory f = new CarFactory();
f.repair(new CarFactory());
前提:该对象调用完,一个方法之后,这个对象就不再使用了
面向对象三大特征:1 封装 2 继承 3 多态
关键字:权限修饰符:控制变量等其他东西的可见范围
public : 公共的。public修饰的成员变量或方法任何人都可以访问
private: 私有的。private修饰的成员变量或方法只能在本类中进行直接访问
protected: 受保护的。protected修饰的成员变量或方法,可以在自己的继承类,或者同一目录package下的的class里使用
不写权限修饰符:可以在本类或者同package的类里使用
|
权限修饰符 |
当前类 |
同一package |
子孙类 |
其他package |
|
public |
true |
true |
true |
true |
|
protected |
true |
true |
true |
false |
|
不写 |
true |
true |
false |
false |
|
private |
true |
false |
false |
false |
封装的步骤:
1 使用private修饰需要被封装的属性
private String sex;
2 提供一个公共的方法来设置或获取该私有成员的属性
命名规范是 set属性名() get属性名()
class People{ public String name; private String sex; public void setSex(String val) { if(val.equals("male") || val.equals("female")) { //比较两个字符串,不要使用==,而要用str.equals()方法来比较 sex = val; }else{ sex = "male"; } } public String getSex() { return sex; } }
注意字符串判断是否相等不要用==号,因为引用类型变量==号是判断内存地址是否相同。而要用str.equals()方法来判断
People p1 = new People(); p1.setSex("male"); System.out.println(p1.getSex());
疑问:
封装必须提供get或set方法吗?
不一定,根据需求而定(比如年龄Age, 不希望被人查看,只提供set方法,而不提供get方法)
在现实开发中,一般实体类的所有成员属性(成员变量)都要封装起来。
实体类: 用于描述一类事物的称为实体类,比如成员,汽车,学生
工具类:Arrays 就不是实体类,String字符串类等
封装的优点:
1 提供数据安全性
2 操作简单
3 隐藏与实现
数组的操作练习:
目前存在数组:int[] arr = {0, 0, 12, 1, 0, 4, 6, 0}, 编写一个函数接受一个数组,把数组内的0清空,返回一个不存在0元素的数组
步骤:
1 计算新数组的长度。原数组长度减去0的个数。
2 创建一个新的数组,把非0的数存放到新的数组中。
public static int[] clearZero(int[] arr) { int count = 0; //记录0的个数 for(int i = 0; i < arr.length; i ++) { if(arr[i] == 0) { count ++; } } int[] ret = new int[arr.length - count]; int index = 0; for(int i = 0; i < arr.length; i ++) { if(arr[i] != 0) { ret[index] = arr[i]; index ++; } } return ret; } }
构造函数:
作用:给对应的对象进行初始化
格式
修饰符 函数名(形参) {......}
注意:1 构造函数是没有返回值类型的
2 构造函数名必须与类名一致
class Baby{ public String name; private int age; public String sex; public Baby(String name, int age, String sex){ this.name = name; this.age = age; this.sex = sex; } public void setAge(int val) { this.age = val; } public int getAge() { return this.age; } }
使用构造函数
public static void main(String[] args) { Baby b = new Baby("alex", 1, "male"); Baby b1 = new Baby("lily", 1, "female"); System.out.println(b1.getAge()); }
3 构造函数并不是由我们手动调用的,而是创建对应的对象的时候,JVM就会主动调用到对应的构造函数。并不是b1.Baby()调用的
疑问:创建对象时,JVM会调用对应的构造方法,那么以前创建对象时,JVM是否也会调用到构造方法呢?
答案:会调用。javac(java编译器在编译时加上去的)
验证:javac编译时产生了class文件后,对该文件进行反编译
JDK提供了反编译开发工具:
javap -c -l -private 类名文件
忘记用法,可以在cmd里用javap来查询参数
4 如果一个类没有显式地写上一个构造方法时,那么javac编译器会为该类添加一个无参的构造函数
5 如果一个类已经显式地写上了一个构造函数,那么javac编译器不会再添加一个无参的构造函数
Java编译器添加的构造方法与类的权限修饰符一致
如果我们定义了构造函数
Baby b1 = new Baby(11, “Tom”);
Baby b2 = new Baby();
就会报错,因为有了带参的构造函数,b2这里实参个数不足。有了自定义的构造函数后,javac编译器就不会再加一个无参构造函数了。那么我们可以自己再定义一个无参构造函数即可。
6 构造函数可以在一个类中以函数重载的形式存在多个。
构造函数与普通函数区别:
返回值:
- 构造函数无返回值类型
- 普通函数:有返回值类型,即使无返回值,也写void
函数名:
- 构造函数与类名一致
- 普通函数:命名符合标识符即可
调用方法:
- 构造:在创建对象时由JVM调用
- 普通:由使用对象调用的,一个对象可调用多次普通函数
作用:
- 构造函数:作用于初始化一个对象
- 普通函数:用于描述一类事物的公共行为
构造代码块:
作用:给对象进行统一的初始化, 每次创建对象都会执行这个构造代码块,相当于把这个构造代码块放入相应构造函数之前执行
构造函数的作用: 给对应的对象初始化
格式:构造代码块大括号必须位于成员变量的位置(类内,方法之外)
注意:构造代码块,大括号,必须位于成员变量位置(类之内,方法之外)
class Baby{ public String name; int age; String sex; { System.out.println("构造代码块, 执行了"); } public Baby(String name, int age, String sex){ this.name = name; this.age = age; this.sex = sex; } public Baby() { }
构造代码块是在创建对象的时候执行的。因为各个构造函数会有很多相同的代码,把这些代码写到构造韩;
代码块的类别:
- 构造代码块:在构造函数或者成员变量的位置
- 局部代码块:大括号位于方法之内的。作用:缩短局部变量生命周期,节省内存。
- 静态代码块
public static void main(String[] args) { {//局部代码块 int i = 10; } System.out.println(i);//访问不到会报错 }
局部代码块很少使用。构造代码块比较常用
成员变量显式赋值,构造函数,构造代码块三者的优先级?
public class Demo1 { public static void main(String[] args) { AAA aaa = new AAA(); System.out.println(aaa.i);//300000 } } class AAA{ public AAA(){ //构造函数 i = 300000; } {//构造代码块 i = 200000; } int i = 1000000; //成员变量初始化 }
在javac编译的时候会调整三者的顺序,把成员变量赋值语句int i = 100000; 放到最前边
构造代码块注意事项
1 javac编译器在编译Java代码是,会把成员变量的声明语句提前至一个类的最前端,这样就不会出现变量未声明就是用报错了
2 成员变量的初始化工作其实都是在构造函数中完成的。构造代码块也是,一编译,就会把构造代码块移动到函数中执行
3 一旦编译后,构造代码块就会被转移到构造函数中执行。构造函数中的代码是最后执行的。
所以无论怎么写,javac编译后的顺序:
显示赋值 / 构造代码块 ->构造函数
4 成员变量的显式初始化和构造代码块是按照代码顺序执行的。
public class Demo1 { public static void main(String[] args) { AAA aaa = new AAA(); System.out.println(aaa.i);//1000000 } } class AAA{ {//构造代码块 i = 200000; } int i = 1000000; }
所以就是只有成员变量的显式初始化和构造代码块时是按照自己的顺序执行的
This关键字

结果为 老鼠在吃
存在同名的成员变量和局部变量时,在方法的内部访问的是局部变量,Java采取的是就近原则的机制来访问的。
上述的内存机制
栈内存又称为方法栈。一个方法要运行的时候,JVM就会在栈内存中开辟一片空间属于这个方法,让这个方法在这片空间中执行。堆内存里保存的是引用类型也就是这个新创建的对象的属性

在栈内存空间中找name变量,找不到再去堆内存中找
this关键字代表了所属函数的调用者对象
比如Animal a = new Animal() 那么this就代表了a这个对象
所以this.name就表示这个对象本身的成员变量属性,而不再是方法的局部变量了
This关键字作用:
1 如果存在同名的成员变量和局部变量,那么在方法内部是默认访问局部变量的数据,可以通过this关键字指定访问成员变量的数据
this 关键字注意:
2 如果在一个方法中,访问了一个变量,该变量只存在成员变量的情况下,java编译器会在该变量的前边添加this关键字
this最经典的用法:
用法1:构造函数:
public class Demo1 { public static void main(String[] args) { Student stu = new Student(101, "alex"); System.out.println(stu.name + stu.id); } } class Student{ int id; String name; public Student(int id, String name){ this.id = id; this.name = name; } }
This关键字作用2:在一个构造函数中可以调用另外一个构造函数初始化对象. 至于调用哪个构造函数则要看this(.......)括号中的参数来定。适于构造函数的重载情况。
class Student{ int id; String name; public Student(int id, String name){ this(name);//调用只有一个参数name的构造函数 this.id = id; } public Student(String name) { this.name = name; } }
若加入一个this() ; 再定义一个public Student(){}空的构造方法,则this()会调用这个无参的构造方法。this()只是调用本类的构造方法但具体调用本类哪个构造方法,则要看参数来定,如果括号中有哪些参数,就调用只包含这些参数的构造方法
This关键字的作用:
- 访问成员变量
- 调用本类的另一个构造方法 this(参数)
this 关键字调用其他构造函数注意:
- this关键字在构造函数内调用其他构造函数时,this语句必须位于构造函数的第一句
- this关键字在构造方法中不能出现相互调用的情况,因为是死循环
定义一个人类,并且有属性(姓名,年龄),并且有方法可以比较年龄
public class Demo1 { public static void main(String[] args) { Student stu = new Student(101, "alex", 19); Student stu1 = new Student(102, "bob", 20); System.out.println(stu1.olderThan(stu)); } } class Student{ int id; String name; private int age; public Student(int id, String name, int age){ this(name);//调用只有一个参数name的构造函数 this.id = id; this.age = age; } public Student(String name) { this.name = name; } public void setAge(int val){ this.age = val; } public int getAge() { return this.age; } public boolean olderThan(Student s) { if(this.age > s.getAge()) { return true; }else{ return false; } } }
Static 关键字
static 修饰的成员变量(静态成员变量)只会在数据共享区中维护一份,而非静态成员变量会在每个对象中都维护一份
static String nation = "US"; // 如果所有学生的国籍都一样, 那么就可以只维护一份静态成员变量, //静态成员变量static 可以用类或对象调用
Static 静态修饰符
1 static修饰成员变量:若有数据需要共享给所有对象使用,可用static修饰, 一旦改变, 所有都变
静态成员变量的访问方式:
- 可用对象访问 对象.变量名 s1.nation
- 可用类名访问 类名.变量名 Student.nation
Student s1 = new Student(101, "alex", 12); Student s2 = new Student(102, "lily", 13); System.out.println(s1.nation); // US System.out.println(Student.nation); // US
对于static的理解
static String nation = "US";
然后我们定义变量
Student s1 = new Student(101, "alex", 12); Student s2 = new Student(102, "lily", 13); s1.nation = "China"; System.out.println(s1.nation); // China System.out.println(s2.nation); // China System.out.println(Student.nation); // China
虽然我们只修改了s1.nation但是因为nation是static的,每个变量都是共享内容的。所以每个变量都被改成了China
注意:
- 非静态成员变量只能用对象访问,不能用类名访问
- 不要为了方便而用static修饰数据,只有成员变量有需要被共享数据时才需要static修饰。
- static修饰成员变量的应用场景:如果一个数据需要被所有成员变量共享的时候
需求:统计一个类被使用了多少次来创建对象。该类对外要提供被创建的次数
public class Demo1 { public static void main(String[] args) { Emp e1 = new Emp(); Emp e2 = new Emp(); Emp e3 = new Emp(); System.out.println(Emp.getCount()); } } class Emp{ private static int count = 0; public Emp(){ count ++; } public static int getCount() { return count; } }
这用static修饰了这个成员方法,返回了count计数。每次创建Emp对象都会调用构造方法,在构造方法内count ++
也可以把这个count++放入构造代码块,因为无论有多少构造函数,都会执行这个count++
class Emp{ private static int count = 0; { count ++; } public static int getCount() { return count; } }
静态函数:
static修饰方法(函数)静态成员方法
静态方法的访问:
1 使用对象访问
对象.静态函数名()
2 使用类名访问
类名.静态函数名()
注意:
1 静态函数是可以用类名,或者对象进行访问的,而非静态函数只能用对象调用
静态成员(静态成员变量,静态成员函数)都可以用对象, 类来访问,推荐用类访问
用类来访问可以节省内存
2 静态函数可以直接访问静态成员,但是不能直接访问非静态成员(变量, 方法)
3 非静态函数是可以访问静态和非静态成员, 因为有对象的时候,静态和非静态成员早都存在了
4 静态函数不能出现this或super关键字(相当于调用了非静态成员), 使用类名的时候对象还不存在
原理:上述原因,归根结底是:静态成员变量/方法都是先于对象存在的
构造代码块{...}是在对象创建的时候生成的
静态代码块static {...} 在Student.class类文件加载到内存的时候产生的,不需要创建对象而产生
Static 什么时候修饰一个函数:
如果一个函数没有直接访问到非静态成员时,那么就可以使用static修饰了,就好像python中在pycharm里写方法但没有用self,那么就会提示可以用静态方法
静态函数不能访问非静态成员?
可以访问,但是不能直接访问。
public static int getCount() { Emp e = new Emp(); System.out.println(e.id); }
在静态方法中创建了新的对象,那么也可以访问这个对象的非静态数据了
静态成员和非静态成员区别:
生命周期:静态成员随类的加载而存在,随类文件的消失而消失
非静态成员随对象的创建而存在,随对象被垃圾回收器回收而消失
main 函数详解:
public: 公共的,权限是最大的,在任何情况下都可以访问(由JVM调用main方法)
原因:为了保证让JVM在任何情况下都能访问到main函数,如果用private,那么出了类就无法访问到主函数了
static:静态的,可以让JVM调用main函数更方便,不需要通过对象调用,用类调用就可以了
不适用static修饰的麻烦:1 需要创建对象调用
2 JVM不知道如何创建对象,因为创建对象是需要参数的,参数传递什么呢?
void 无返回值
main 函数名,并不是关键字,只不过是JVM能识别的一个特殊的函数名而已
arguments 参数,形参,String[] arguments 字符串数组类型
public static void main(String[] args) { System.out.println(args.length); }
在控制台编译之后javac Demo4 之后使用
使用Java虚拟机运行时java Demo4 aa bb cc
就可以得到长度为3
提供了String[] args主要是为了在代码运行之初接受参数,现在基本不用了,因为scanner可以实现接受用户输入。因为实际过程中并不止让用户输入一次。而且args对于输入没有提示信息
单例设计模式single design pattern:
模式:模式是解决一类问题的固定步骤
起源于建筑行业,软件行业中有23种模式:
单例设计模式(懒汉, 饿汉),模板设计模式,装饰者设计模式,观察者设计模式,工厂设计模式
单例设计模式步骤(饿汉单例设计模式)
1 私有化构造函数
2 声明本类的引用类型变量,并且使用该变量指向本类对象
3 提供一个公共的静态的方法,获取本类对象(获取的是对象,返回值为类名的类型)
class Single { private static Single s = new Single(); //声明本类的应用类型变量 private Single(){//私有化构造函数,外部不能用new声明,因为构造函数不可见 } public static Single getInstance(){ //提供公共静态的方法 return s;//而且拿到的对象是唯一的 } } public static void main(String[] args) { Single s1 = Single.getInstance(); Single s2 = Single.getInstance(); System.out.println(s1 == s2);//true, 内存地址相同,引用类型变量用==号比较地址是否相同 }
这样定义了以后,一旦JVM运行类文件的时候,就会立即产生这个例子,这样就有些浪费内存。
懒汉单例设计模式:
1 私有化构造函数
2 声明本类的引用类型变量,但不要创建对象 Single s;
3 提供公共静态方法获取本类的对象,获取之前先判断是否已经创建了本类对象,如果已经创建了,那么久直接返回这个对象。如果还没创建,那么久先创建对象再返回
class Single { private static Single s; //声明本类的应用类型变量 private Single(){//私有化构造函数,外部不能用new声明,因为构造函数不可见 } public static Single getInstance(){ //提供公共静态的方法 if(s == null){ // 判断这个对象存在否 s = new Single(); // 如果不存在就创建对象 } return s;//而且拿到的对象是唯一的 } }
推荐使用:饿汉单例设计模式。因为懒汉单例设计模式会存在线程安全问题,目前还不能保证一个类在内存中只有一个对象
CPU在一个线程片中只能执行一个线程。如果两个线程在if语句这里发生了抢占CPU的行为,每个线程都执行了if判断条件,a和b线程发现s是空的,那么a线程向下创建了一个,b线程抢过CPU后也创建了一个,这时候就创建了两个
继承:
事物之间的关系:组合关系(整体与部分的关系),在java中称为has a关系
继承关系:is a的关系,关键字extends
继承格式:
class 子类名1 extends 父类名2 {}
class Student extends People{
//这样就确认了两个类的继承关系,子类Student继承了父类People
}
People 也称为 父类,基类,超类
重复的代码(声明变量,属性)删掉,不删掉会出问题,父类有一个无参的构造函数
继承的注意事项:
- 不要为了减少重复代码而继承,只有真正存在继承关系的时候才去继承
- 父类中私有成员private不能被继承。在父类中private int age如果在main中尝试用s.age = 20;报错,private的只能在类中使用,不能被子类继承
- 父类的构造函数不能被继承
- 创建子类对象时默认会先调用父类的无参构造函数
class People{ String name; int age; public People(){}//父类必须要有一个无参构造方法, public People(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(this.name + " is eating"); } } class Student extends People{ int id; // People类中已经有了name和age了,就不用再定义了 public Student(int id, String name, int age) { super(name, age); this.age = age; } public void study() { System.out.println(this.name + " is studying"); } }
为什么要调用父类的构造方法呢?
子类继承了父类以后,我们使用子类创建对象,并没有用父类创建对象。子类有很多属性方法是继承自父类的属性和方法。如果我们继承了父类的非静态成员,那么没有创建父类的对象,如何使用这些属性呢?
只能先调用父类的构造方法,初始化从父类继承下来的属性。
做法:
- 先给子类一个带参的构造方法,包含有要初始化的属性
- 父类也有一个构造方法(相同参数或者有几个参数的构造方法)
- 子类构造方法中写super(参数1,参数2,......)就可调用父类中形参列表一致的构造方法
super关键字:指的是父类空间的引用
super关键字的作用:
1 子父类存在同名成员时,在子类中默认访问的是子类的成员,可以通过super访问父类成员。
super.父类属性()
super.父类方法()
2 创建子类对象时,默认会先调用父类无参构造方法,可以通过super关键字指定调用父类的构造方法
super(name, age) 调用父类的有两个参数name和age的构造函数
super关键字调用父类方法注意:
1 如果子类的构造方法中,没有指定调用父类的构造方法,那么编译器会在子类构造方法中加上super()
2 super关键字调用父类构造函数时,该语句必须要为子类构造函数中第一句
3 super和this不能同时出现在同一个函数中调用其他构造函数。因为两个语句都需要置于第一句。
super 关键字与this 关键字的区别:
1 代表对象不一致:
- super:代表父类空间的引用
- this:代表所属函数的调用者对象
2 使用前提:
- super关键字要有继承关系才能使用
- this关键字不需要继承关系也可使用
3 调用构造函数的区别:
- super:调用父类的构造函数
- this:调用本类的构造函数
若父类只有带参构造函数,编译器不会给他加无参构造函数,而子类中又没有构造方法。这样就会默认调用父类无参的构造函数,找不到就报错。所以父类最好有个无参构造函数
子父类出现了同名函数,就要方法重写。
父类的功能无法满足子类的需求时,就应该使用方法的重写。
方法的重载:在一个类中,存在两个或以上的同名函数(函数名一致,形参列表不一致)
方法的重写:前提是必须有继承
方法重写注意事项:
1 方法名与形参列表必须和父类的一致
2 方法重写时,子类的函数权限修饰符 >= 父类的 父类是private,子类是public无法改写
3 方法重写时,子类的返回值类型<= 父类返回值类型
4 方法重写时,子类抛出的异常类型<=父类抛出的异常类型
Exception(最坏) Runtime Exception(小坏) 一代比一代好
父亲杀人放火,儿子偷鸡摸狗是好事,一代要比一代好
5 如果方法重写时,是觉得父类的方法不够用,想再多一些内容,那么就可以在方法中加上
super.study()来表示先执行父类的同名方法
public class Demo1 { public static void main(String[] args) { BaseStudent bs = new BaseStudent("alex", 101, 12); CareerStudent cs = new CareerStudent("bob", 102, 13); bs.study(); cs.study(); System.out.println(bs instanceof Student); } } class Student{ String name; int id; int age; public Student(){} public Student(String name, int id, int age){ this.name = name; this.id = id; this.age = age; } public void study(){ System.out.println(this.name + " Math"); } } class BaseStudent extends Student{ public BaseStudent(String name, int id, int age){ super(name, id, age); } public void study(){ super.study();//在父类的.study方法基础上再执行别的操作 System.out.println(this.name + " is learning Java se"); } } class CareerStudent extends Student{ public CareerStudent(String name, int id, int age){ super(name, id, age); } public void study(){ super.study(); System.out.println(this.name + " is learning Java EE + Android"); } }
instanceof 关键字
用来判断一个对象是否属于指定的类别
instancof 关键字使用前提,判断的对象与指定的类别必须要存在继承或实现的关系
使用格式:
对象 instanceof 类型
instanceof 在多态中非常有用。一般我们做强制类型转换前都会使用该关键字判断再转换的
final 关键字
public static final double PI = 3.14;
1 final关键字修饰一个基本类型的变量时,该变量不能重新赋值,第一次的值为最终的。使用final修饰的话不可以被重新赋值
final double PI = 3.14 声明之后就要立刻赋值,之后不能再修改了
2 final关键字修饰一个引用类型变量时,该变量不能重新指向新的对象
public class Demo1 { public static void main(String[] args) { final Circle c = new Circle(10); System.out.println(c.getArea()); // c = new Circle(5);//报错,final关键字修饰的引用类型变量c不允许再重新指向新的引用类型变量 } } class Circle{ public static final double PI = 3.1415; double r; public Circle(double r){ this.r = r; } public double getArea() { return this.r * this.r * PI; } }
每个方法要运行的时候,JVM都会为该方法开辟一片内存空间是属于该方法的,方法中的形参也是属于该方法的局部变量
另一种情形是允许的。
final circle c = new Cirle(4.0); test(c); // 传递的是变量存储的值,而非变量本身 public static void test(Circle c){ c = new Circle(5.0);//这样不会报错的,这个c已经是test方法的形参了,不是上边的c c.getArea(); }
每个方法要运行的时候,JVM都会为该方法开辟一片内存空间是属于该方法的,方法中的形参也是属于该方法的局部变量。所以每个方法内的局部变量即使重名也毫无关系。上边的test里的c是局部变量,和外部的final Circle c无关
3 final关键字修饰一个函数时,该函数不能被重写
4 final关键字修饰一个类时,该类不能被继承。
所以final的特点就是,最终的,无法修改,无法被继承
一个变量的值不能再修改就是常量;
常量修饰符一般是 public static final PI = 3.14
public 谁都能访问 static 只存一份 final不可修改
抽象类:
我们在描述一类事物时发现该事物确实存在着某种行为(狗跑,鱼游,鸟飞),虽然都是移动行为,但是不同的动物描述是不同的,需要让子类去重写这些方法。这种行为目前不具体,那么我们可以抽取这种行为的声明,但是不去实现这种行为,这种行为我们称为抽象行为没我们需要抽象类
public void run(){ System.out.println(name + "跑"); } public void run() { System.out.println(name + "飞"); }
函数定义的时候,只要有大括号那就是具体的函数体。
抽象类:
1 如果一个函数没有方法体,那么该函数必须使用abstract修饰,把该函数修饰成抽象的函数
Public abstract void run ();
2 如果一个类出现了抽象的函数,该类也必须使用abstract修饰,abstract class Animal{...}
3 如果一个非抽象类继承了抽象类,那么必须把抽象类的所有方法都实现。
抽象类的好处:强制要求子类一定要实现一些方法
4 抽象类可以存在非抽象方法,也可以存在抽象方法
5 抽象类可以没有抽象方法(语法是支持的),但是不建议抽象类里都是具体方法,没有意义
6 抽象类时不能创建对象的 Animal a = new Animal()
7 抽象类是存在构造函数的(虽然抽象类不能创建对象,也无从谈起初始化) 其构造函数是提供给子类创建对象的时候,初始化父类属性的
如果抽象类可以创建对象,那抽象的方法无方法体就不知道该执行什么内容了
在python中也是有抽象类的,但是需要模块abc来导入
描述一个图形类,圆形,矩形类,不管哪种图形,都具备计算面积与周长的行为,只不过每种图形计算方式不同
abstract class Shape{ double width; double height; double r; public static final double PI = 3.1415; public Shape(double width, double height){ this.width = width; this.height = height; } public Shape(double r) { this.r = r; } public abstract double getArea(); public abstract double getPerimeter(); } class Circle extends Shape{ public Circle(double r) { super(r); } public double getArea(){ System.out.println(this.r); return Shape.PI * this.r * this.r; } public double getPerimeter(){ return 2 * Shape.PI * this.r; } } class Rect extends Shape{ public Rect(double width, double height){ super(width, height); } public final double getArea() {// 不想让别人修改这个函数,所以定位final return this.width * this.height; } public final double getPerimeter() { return 2 * (this.width + this.height); } }
abstract 不能与以下关键字(private, static, final)共同修饰一个方法:
1 private:
private abstract void getArea(); 报错,私有的一旦出了类就不可见了,而抽象方法必须在子类中实现,自相矛盾
2 static:
public static abstract void run(); 报错,因为一旦static修饰后,就可用类名调用,而抽象方法没有函数体,所以无法执行
3 final:
public final abstract run(); 报错,因为一旦final修饰后就无法更改,所以抽象方法必须要子类中重写,自相矛盾了
值交换:调用一个方法的时候,传递给方法的变量实参,实际上传递的只是变量所存储的值。
public class Demo1 { public static void main(String[] args) { int a = 3; int b = 5; valueExchange(a, b); System.out.println("a = " + a + " b = " + b); // 结果任然是3和5并未交换 } public static void valueExchange(int a, int b) {//这些局部变量都是在该方法的栈空间内生成的,和外部毫无关系 int temp = a; a = b; b = temp; }//原因是出了这个作用域之后,变量就消失了 }
重点:
1 形式参数是数据所属函数的局部变量
2 不同函数的局部变量与局部变量是相互独立的,没有任何关系
交换两个数组元素的位置
public static void main(String[] args) { int[] a = {3, 4, 6, 1, 7, 8}; eleExchange(a, 2, 5); System.out.println(Arrays.toString(a));//交换成功 } public static void eleExchange(int[] arr, int i, int j) { // 交换arr数组中第i和j元素的位置 arr[i] = arr[i] ^ arr[j]; arr[j] = arr[i] ^ arr[j]; arr[i] = arr[i] ^ arr[j]; }
第一次,基本变量我们按值来传递,所以只把变量的数值传给了局部变量,结果局部变量间毫无关系,无法成功交换。第二次,引用类型变量,我们传递引用,也就是堆内存的地址给函数变量,把数组中的元素的地址交换了
第二次里边,两个函数arr是不同的。arr两个不同的变量但是操作了相同的对象,所以才修改成功。
如果是不同的引用类型变量,操作同一个对象,那么肯定会影响到结果(因为他们直接按堆内存地址来操作)
这与基本类型的值交换不同,基本类型值传递是按值得,也就是栈内存。
继承补充:
一个类最多只能有一个直接的父类,但是可以有多个间接的父类。Java是单继承的(只能直接继承一个类)
class son extends father, grandpa{} 这是会报错,只能继承一个,但是father可以继承grandpa,形成一个继承链,那么son也可以继承到grandpa的属性
接口:
因为java是单继承,所以不可以一次继承多个类,如果这些类互相也没有关系,那么如何具备他们的功能呢?可以用接口来解决
定义格式:
interface 接口名{......}
接口的注意事项:
1 接口是一个特殊的类,(无构造函数的全abstract方法的全public static final变量的,抽象类)
2 接口的成员变量默认的修饰符public static final, 那么也就是说接口中的成员变量都是常量, 如果3个修饰符漏写了一个或全部,java编译器都会自动补全,可用javap -c -l -private反编译来验证
3 接口中的方法都是抽象方法,默认修饰符为public abstract. 如果你不写abstract,也会自己给你加上
4 接口方法是抽象的,所以接口也是抽象的,因此接口不能实例化对象
5 接口是没有构造方法的(成员变量都是常量,也不可能给子类什么的初始化) 继承的时候,子类调用父类的构造方法是为了初始化父类那些变量的
6 接口是给类去实现使用的,非抽象类实现一个接口的时候,必须要把接口中所有的方法全部实现
实现接口的格式:
class 类名 implements 接口名{ //实现接口 }
带橡皮的铅笔
public class Demo1 { public static void main(String[] args) { PencilWithEraser p = new PencilWithEraser("pencil"); p.remove(); p.write(); } } class Pencil{ String name; public Pencil(String name) { this.name = name; } public void write() { System.out.println(this.name + " is used to write"); } } interface Eraser{ public abstract void remove(); } class PencilWithEraser extends Pencil implements Eraser{ public PencilWithEraser(String name) { super(name); } public void remove() { System.out.println(this.name + " is removing"); } }
接口的功能:
1 拓展功能(有接口就可以让某类实现相关功能,提供代码复用性,这和函数功能很像)
2 定义约束规范,通常一个网站有好几个模块,由项目组长定义接口,下层开发人员不能随便给方法起名字,这样约束了规范
3 程序的解耦(最重要) (程序低耦合)
类与接口的关系:实现关系
类与接口注意:
- 非抽象类实现一个接口时,必须要把接口中的方法都实现
- 抽象类实现一个接口时,可以实现接口中的方法,也可以不实现(因为抽象类自己本身里面也有很多抽象方法)
- 一个类可以实现多个接口
class Animal implements A, B {......}
类与类:单继承
类与接口:多实现
接口与接口:多继承 interface B extends A {}
疑问:Java为什么是单继承,多实现?
为什么不是多继承?继承的两个类里都有同名方法,JVM不知道调用哪个
多实现就没问题。因为每个接口方法只有抽象名字,而无方法体。具体方法内容要在实现的接口的类中完成
一个接口是可以继承多个接口的。多继承用,号 interface B extends A, C {}
多态:
多态:一个对象具备多种形态。父类的引用类型变量指向了子类的对象。或者接口的引用类型变量指向了几口实现类的对象
动物 a = new 狗(); 这样的说法是合法的,你要一个动物,我给你一只狗符合要求啊!
public class Demo1 { public static void main(String[] args) { People p = new Student("alex", 12); // 父类的引用类型变量,指向了子类的对象 p.run(); } } class People { String name; int id; public People(String name, int id) { this.name = name; this.id = id; } public void run() { System.out.println(this.name + " is running"); } } class Student extends People { public Student(String name, int id) { super(name, id); } public void run() { super.run(); } }
多态注意的细节:
- 多态情况下,子父类存在同名成员变量时,访问的是父类的成员变量,不管是静态还是非静态。这很好理解,因为是 父类 变量名 = new 子类() 结果还是父类类型的
- 多态情况下,子父类存在同名的静态成员函数时,访问的是父类的成员函数
- 多态情况下,子父类存在同名非静态成员函数时,访问的是子类的成员函数
- 多态情况下,不能访问子类特有的成员
总结:多态情况下,子父类存在同名成员(变量和方法),访问的都是父类成员。只有在同名非静态函数时才是访问子类的
关于第4点:
Java编译器 编译看左边,运行不一定看右边。
Java编译器在编译时,会检查引用类型变量所属的类是否具备指定成员,如不具备,马上报错
多态 Animal a = new Dog(); a.dig() 编译时会检查左边Animal类型是否含有指定的成员.dig(),如果没有就报错。
多态的应用:
1 多态应用于形参类型时,可以接受更多类型数据
需求 1: 定义一个函数,可以接受任意类型图形对象,并打印面积与周长
public class Demo1 { public static void main(String[] args) { Circle c = new Circle(12); Rect r = new Rect(10, 20); print(c); print(r); } //定义一个函数,接受任意类型的图形对象,并打印图形面积 //如果没有多态,那么我们只能用函数的重载来做. 如果再有更多的图形要写很多重载 public static void print(Shape s) { s.getArea(); s.getPerimeter(); if(s instanceof Circle) { System.out.println("This is a circle"); }else if(s instanceof Rect) { System.out.println("This is a Rectangular"); }else{ System.out.println("Other shapes"); } } } abstract class Shape{ public abstract void getArea(); public abstract void getPerimeter(); } class Circle extends Shape{ public static final double PI = 3.14; double r; public Circle(double r) { this.r = r; } public void getArea() { System.out.println(PI * this.r * this.r); } public void getPerimeter() { System.out.println(2 * PI * this.r); } } class Rect extends Shape{ double width; double height; public Rect(double width, double height) { this.width = width; this.height = height; } public void getArea() { System.out.println(this.width * this.height); } public void getPerimeter() { System.out.println(2 * (this.width + this.height)); } }
在主函数那里,实际上就是把new Circle() 和 new Rect()传给了Shape s。所以这就是把父类的引用类型变量指向了子类的对象
多态的好处:
- 提高了代码的拓展性
- 多态用于返回值类型时,可以返回更多类型数据
需求2:定义一个函数,可以返回任意类型的图形对象
public class Demo1 { public static void main(String[] args) { Shape m = getShape(1); m.getArea(); m.getPerimeter(); } public static Shape getShape(int i) { // 函数既可以返回圆形,也可以返回矩形 if(i == 0) { return new Circle(4.0); }else { return new Rect(3.0, 4.0); } } }
让函数返回Shape类型,相当于Shape s = new Circle()或new Rect()
所以多态里边,要接受不同类型的数据,那么形参要写他们的父类;同理要返回不同类型数据时,返回值类型也要写父类。并用父类的变量来接收
多态最不方便的地方就是不能调用子类特有的成员。
如果多态情况下需要访问特有的方法,那么就需要强制类型转换。
所以就将这个父类的引用变量强制转换回子类 (子类) 父类引用类型变量
小数据类型---->大数据类型 自动转换
大数据类型---->小数据类型 强制转换
引用数据类型的转换:
小数据类型就相当于子类,大数据类型相当于父类
小数据类型---->大数据类型 自动转换
大数据类型---->小数据类型 强制转换
Animal a = new Mouse(“老鼠”); 这时候不能调用子类特有属性
Mouse m = (Mouse) a; 类型强转之后,就可以调用子类的特有属性了
public class Demo1 { public static void main(String[] args) { Animal a = new Mouse("mouse", 1);//父类的引用类型变量指向了子类的对象, // 或者接口类的引用类型变量指向了实现类的对象 // a.dig();//报错无法调用子类特有属性 Mouse m = (Mouse) a; m.dig();//强制类型转换以后就可以调用子类特有的方法了 } } class Animal{ String name; int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(this.name + " is eating"); } } class Mouse extends Animal{ public Mouse(String name, int age){ super(name, age); } public void dig(){ System.out.println(this.name + " is digging a hole"); } }
java.land.ClassCastException 数据类型转换失败
需求:定义一个函数,可接受任意类型动物对象,在函数内部要调用到动物特有的方法
public static void get(Animal a) { if(a instanceof Fish){ Fish f = (Fish)a; f.swim(); }else if(a instanceof Mouse){ Mouse m = (Mouse)a; m.dig(); } }
先判断是否是类型再强制转换
接口关系下的多态
接口 变量 = new 接口实现类的对象
接口关系下的多态都是调用实现类的方法:
public class Demo1 { public static void main(String[] args) { Dao d = new UserDao(); d.add(); d.delete(); } } interface Dao{ public abstract void add(); public abstract void delete(); } class UserDao implements Dao{ public UserDao(){} public void add(){ System.out.println("add successfully"); } public void delete(){ System.out.println("deleted successfully"); } }
为什么接口关系下的多态都是调用实现类的方法,
因为接口关系中的方法都是非静态的方法,不能用static,因此也无法用接口类来调用,调用也没有意义,抽象方法没有方法体
内部类:
一个类定义在另外一个类的内部,那么该类就称为内部类。
class Outer{//外部类 class Inner{//成员内部类 } }
内部类的class文件名是,外部类$内部类.class
内部类有成员内部类和局部内部类:
- 成员内部类:内部类在在外部类的内部,与成员变量处于同一级别位置
- 局部内部类:内部类在方法中
class Outer{//外部类 int i; public void test() { class Inner{ //局部内部类 } } }
成员内部类:也可以定义自己的成员变量和成员方法
class Outer{//外部类 class Inner{ int i;//成员内部类的成员变量 public void test() { //成员内部类的成员方法 } } }
成员内部类的访问方式:
class Outer{//外部类 class Inner{ int i;//成员内部类的成员变量 public void test() { //成员内部类的成员方法 } } public void instance() { Inner inner = new Inner();//外部类的方法中创建了内部类的对象 inner.test();//调用内部类的方法 } }
在main函数中
public static void main(String[] args) { Outer.Inner inner = new Outer().new Inner(); inner.test(); }
访问方式1:外部类中提供一个方法来获得内部类的对象
Outer outer = new Outer();
outer.instance();
定义外部类的对象,并调用一个方法来创建内部类对象
访问方法2:
Outer.Inner inner = new Outer().new Inner();
外部类.内部类 变量名 = new 外部类().new 内部类();
静态内部类在其他类的创建对象方式:外部类.内部类 变量名 = new 外部类.内部类(); 对于内部类就是通过外部类的类名访问,无需再创建对象了
解释:new Outer()以后,Inner这个类才会存在在内存中,再进行new Inner()才能解析
内部类应用场景:
内部类的好处:内部类可以直接访问外部类的所有成员;
我们在描述A类事物的时,发现描述的A事物内还有一个比较复杂的事物B,这是B还需要访问A事物的属性等数据,那么这是我们就可以使用内部类描述事物B
比如:人---->心脏的关系,心脏可以访问人体其他属性。一个类进入了另一个类的肚子,想访问哪就访问哪,可以直接访问外部类的所有成员
内部类注意:
1 如果外部类与内部类存在同名成员变量,在内部类中默认访问内部类的成员变量(就近原则)
内部类访问外部类的属性x 外部类.this.成员变量名
2 私有的成员内部类private class Inner只能在外部类提供一个方法创建内部类对象进行访问,不能再其他类创建对象了
3 成员内部类一旦出现了静态成员,那么该类也必须使用static修饰
class Outer{//外部类 static class Inner{ static int i; // 一旦有静态成员变量,内部类也要是静态的 public void test() { //成员内部类的成员方法 } } }
原因:在外部类中class Outer{int x;} 是在文件被加载到内存中时存在在内存中的。静态成员数据是不需要对象存在访问的。如果内部类中class Inner{int x = 20;} 不用static修饰。那么inner必须在外部类对象存在时才能存在。即new Outer().Inner.x 这样有了创建对象,也和设计静态变量产生了冲突
如果是一个静态内部类,那么创建对象的格式:
Outer.Inner inner = new Outer.Inner();
局部内部类:
在一个类的方法内部定义另外一个类, 那么另外的这个类就是局部内部类
class Outer{//外部类 int i; public void test() { class Inner{ //局部内部类 public void print() { //局部内部类方法 } } Inner inner = new Inner(); inner.print();//在方法内部创建局部类的对象 } }
局部内部类和函数的局部变量一样,只能在方法内部使用
局部内部类要注意的细节:
1 如果局部内部类访问了一个局部变量,那么该局部变量必须使用final修饰
public class Demo1 { public static void main(String[] args) { Outer outer = new Outer(); outer.test(); } } class Outer{//外部类 int i; public void test() { final int i = 10; class Inner{ //局部内部类 public void print() { //局部内部类方法 System.out.println(i); } } Inner inner = new Inner(); inner.print();//在方法内部创建局部类的对象 } }
为什么局部内部类访问的局部变量一定要final修饰呢?
当test方法执行完毕时,这个i变量已经从内存中消失,而这个inner对象还未从内存中消失,但此时i变量已经消失了,给人感觉i的生命周期被延长了。
解决:如果一个局部内部类访问一个局部变量时,那么就让该局部内部类访问这个局部变量的复制品。
匿名内部类:没有类名的类就为匿名内部类。属于局部内部类一种,使用时需要外部类对象有一个访问方法来访问匿名内部类。
匿名内部类的好处:简化书写;
使用前提:必须存在继承或实现关系才能用
需求:在方法内部定义一个类继承Animal类,然后调用run方法
public class Demo1 { public static void main(String[] args) { Outer o = new Outer(); o.print(); } } abstract class Animal{ public abstract void run(); } class Outer{ public void print() { class Dog extends Animal{ public Dog(){} public void run(){ System.out.println("狗在跑"); } } Dog d = new Dog(); d.run(); } }
为了简化书写,我们使用匿名内部类:
匿名内部类:只是没有类名而已,其他都有
他需要借助父类或者父接口的名字
class Outer{ public void print() { new Animal(){//匿名内部类与Animal是继承的关系 public void run(){ System.out.println("狗在跑"); } }.run(); } }
匿名内部类继承了父类,要把父类内的抽象方法都实现
new 父类名(){ //匿名内部类的成员变量和函数也存在的。 }.方法名()
匿名内部类格式 new 父类名(){.....} 创建了一个匿名子类对象。之后就可以直接
new 父类名(){......}.方法()
如果一个匿名内部类需要同时调用两个方法.run() .sleep()应该怎么办呢?
方式1:让第一个方法返回值为this,也就是本类的对象,方便第二个方法连缀使用
public class Demo1 { public static void main(String[] args) { Outer o = new Outer(); o.print(); } } abstract class Animal{ public abstract Animal run(); public abstract void sleep(); } class Outer{ public void print() { new Animal(){//匿名内部类与Animal是继承的关系 public Animal run(){ System.out.println("狗在跑"); return this;//这里又返回了对象自身,方便连缀 } public void sleep(){ System.out.println("狗在睡觉"); } }.run().sleep(); } }
方式2:如果同时需要调用两个方法,可以用多态给子类命名再调用
class Outer{ public void print() { Animal a = new Animal(){//多态的形式体现的,new Animal()在这里是继承父类Animal的子类的对象 public Animal run(){ System.out.println("狗在跑"); return this; } public void sleep(){ System.out.println("狗在睡觉"); } }; a.run(); a.sleep(); } }
如果匿名内部类的内部有特有的方法,不在父类abstract类里,那就只能用局部内部类解决了,因为多态不能调用子类特有的方法,而匿名时也无法做类型强转。
实现关系下的匿名内部类:
public class Demo1 { public static void main(String[] args) { User u = new User(); u.test(); } } interface Dao{ public void add(); } class User { int i = 0; public void test() { new Dao(){ //这不是用接口创建对象,这是多态表示接口的实现类的对象new Dao()表示实现了接口的对象 public void add(){ System.out.println("实现关系下,匿名内部类的add方法"); } }.add(); } }
匿名内部类一般是用于实参
public class Demo1 { public static void main(String[] args) { run(new Dao(){ public void test(){ System.out.println("Hello World"); } }); } public static void run(Dao d) { d.test(); } } interface Dao{ public abstract void test(); }
以后开发中,匿名内部类都是作为实参传递,简化书写。
....................................................
浙公网安备 33010602011771号