Java知识点02——面向对象01(类、对象、方法、常量、变量、构造器、包、封装、继承、多态、static关键字)
声明:
- 该资料来自自己整理。
- 参考书籍
疯狂 Java讲义(第五版) 李刚©著
一、类和对象
1.1 面向对象、面向过程
1)面向过程
面向过程
就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向过程, 强调的是功能行为,以函数为最小单位,考虑怎么做。
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。
2)面向对象
面向对象
思维方式是一种更符合人们思考习惯的思想,将复杂的问题简单化。面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:性能比面向过程低。
1.2 类、对象
什么是类
类:是一组相关属性和行为的集合。可以看成是一类事物的模板,使用事物的属性特征和行为特征来描述该类事物
现实中,描述一类事物:
属性:就是该事物的状态信息
行为:就是该事物能够做什么
举例:小猫
属性:名字、体重、年龄、颜色...
行为(方法函数):跑、叫、睡、吃...
什么是对象
对象:是一类事物的具体体现。对象是类的一个实例,必然具备该类事物的属性和行为。
现实中,一类事物的一个实例:一只小猫
举例:一只小猫。
属性:tom、5kg、2 years、yellow...
行为(方法函数):慢悠悠的跑、温柔的叫、呼呼大睡...
类与对象的关系
对象是类的实例,类是对象的模板。
1)类是对一类事物的描述,是抽象的
2)对象是一类事物的实例,是具体的
3)类是对象的模板,对象是类的实例
1.3 类的定义
在java中,我们可以用关键字class来定义一个类,一个java文件可以同时定义多个class。
// 定义一个Person类
class Person {
}
// 定义一个Dog类
class Dog {
}
// 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
public class Student {
// 属性,此处也称为成员变量
String name; // 姓名
int age; // 年龄
// 方法,此处也称为成员方法
void eat(String food) {
System.out.println(name + "在吃" + food);
}
void study() {
System.out.println(name + "年龄" + age + "岁,在学习java");
}
}
类的组成
属性 field
,定义格式: [修饰符] 属性类型 属性名 = [默认值]
;
方法(行为) method
,定义格式: [修饰符] 返回值类型 方法名(形参列表) {}
1.4 创建对象
在java中,对象也叫做 object 或 instance(实例),所以也称为实例。
要想创建一个对象,那么必须先有一个类,然后通过new 关键字
来创建一个对象。(通过new关键字来调用构造器,从而返回该类的实例。如果一个类没有构造器,则这个类无法创建实例。)
语法格式:类名称 对象名称 = new 类名称()
;
// 测试类
public class ObjectTest {
public static void main(String[] args) {
// 实例化小明学生对象
Student stu1 = new Student();
// 成员变量赋值
stu1.name = "小明"; // 设置名字
stu1.age = 18; // 设置年龄
System.out.println("name:" + stu1.name + " age:" + stu1.age);
// 调用成员方法
stu1.eat("小龙虾"); // 调用吃放方法
stu1.study(); // 调用学习方法
// 实例化小花学生对象
Student stu2 = new Student();
// 操作成员变量
stu2.name = "小花"; // 设置名字
stu2.age = 17; // 设置年龄
System.out.println("name:" + stu2.name + " age:" + stu2.age);
// 调用成员方法
stu2.eat("冰淇淋"); // 调用吃放方法
stu2.study(); // 调用学习方法
}
}
注意事项:
1、成员变量隶属于于对象,只能通过对象来调用,我们可以通过对象.成员变量
来操作成员变量。
2、成员方法隶属于于对象,只能通过对象来调用,我们可以通过对象.成员方法(实参列表)
来调用成员方法。
1.5 成员变量的默认值
数据类型 | 默认值 | |
---|---|---|
基本类型 | 整数(byte,short,int,long) | 0 |
浮点类型 | 浮点数(float,double) | 0.0 |
字符型 | 字符(char) | ‘\u0000’ |
布尔型 | 布尔(boolean) | false |
引用类型 | 数组,类,接口 | null |
1.6 对象内存图
在栈内存中运行的方法,遵循 “先进后出,后进先出” 的原则。
Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。不管是数组还是对象,都只能通过引用来访问它们。
//定义一个Person类型的变量p;并将Person实例赋给p变量
Person p = new Person();
p.name = "李刚";
p.age;
二、方法
2.1 方法的定义
修饰符 返回值类型 方法名(参数类型 参数名1,参数类型 参数名2,……){
执行语句;
return 返回值;
}
举例:
public static void play(){
System.out.println("这是一个方法");
}
解释:
修饰符
:目前固定写法 public、private、返回值类型
:void、基本类型、引用类型方法名
:为我们定义的方法起名,满足标识符的规范,用来调用方法参数列表
:return
:方法结束,因为返回值类型是void,方法大括号内的return可以不写
2.2 方法的调用
方法在定义完毕后,方法不会自己运行,必须被调用才能执行,我们可以在主方法main中来调用我们自己定义好的方法。
实际参数(简称实参):传递给形式参数的具体数值,对应着形式参数(简称形参)。
2.2.1 注意事项
注意事项:
- 1、形式参数(形参)和实际参数(实参)的类型和个数必须一一对应。
- 2、在方法中只能调用方法,不可以在方法内部再定义方法。
- 3、当方法有返回值类型时,可以用一个变量用于接收方法的返回值,该变量的类型必须和方法返回值类型保持一致。
案例:
public class MethodT01 {
public static void main(String[] args) {
MethodT01 m = new MethodT01();
//静态方法调用普通方法只能使用该类的实例(对象)来调用!!!
m.eat("晴天");
//静态方法访问静态方法直接调用即可!!!
fly("大波");
}
//普通方法01
public void eat(String name){
System.out.println(name+"吃饭。");
//普通方法调用普通方法使用this作为调用者调用!!!
this.play(name);
//普通方法调用静态方法必须使用类名来作为调用者!!!
MethodT01.fly("小波波");
}
//普通方法02
public void play(String name){
System.out.println(name+"正在玩。");
}
//普通方法03
public void watch(String name){
System.out.println(name+"正在看电视。");
}
//静态方法
public static void fly(String name){
System.out.println(name+"飞行。");
}
}
2.2.2 方法调用的三种形式
直接调用
// 直接写方法名调用
public static void main(String[] args) {
print();
}
public static void print() {
System.out.println("方法被调用");
}
赋值调用
// 调用方法,在方法前面定义变量,接收方法返回值
public static void main(String[] args) {
int sum = getSum(5,6);
System.out.println(sum);
}
public static int getSum(int a,int b) {
return a + b;
}
输出语句调用
// 在输出语句中调用方法, System.out.println(方法名());
public static void main(String[] args) {
System.out.println(getSum(5,6));
}
public static int getSum(int a,int b) {
return a + b;
}
// 不能用输出语句调用 void 类型的方法。因为方法执行后没有结果,也就打印不出任何内容
public static void main(String[] args) {
System.out.println(printHello());// 错误,不能输出语句调用void类型方法
}
public static void printHello() {
System.out.println("Hello");
}
2.3 方法的传参机制
- Java里方法的基本类型的参数传递方式只有一种:值传递。
- Java里方法的引用类型的参数传递,一样是采用:值传递。(这个值是个引用(
指针
),它保存了对象的地址值)
⚠️所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。
2.3.1 基本类型的传参效果
public class PrimiriveTransferTest {
public static void main(String[] args) {
int a = 6;
int b = 9;
swap(a,b);
System.out.println("交换结束后变量a,b的值:a="+a+",b="+b);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("swap方法里a,b的值:a="+a+",b="+b);
}
}
2.3.2 引用类型的参数传递
⚠️创建一个对象时,系统内存中有两个东西:堆内存中保存了对象本身,栈内存中保存了引用该对象的引用变量。
案例:
class DataWrap{
int a;
int b;
}
public class ReferenceTransferTest {
public static void main(String[] args) {
DataWrap dw = new DataWrap();
dw.a = 6;
dw.b = 9;
System.out.println("交换前a,b成员变量的值:a="+dw.a+",b="+dw.b);
swap(dw);//值传递 传递的是一个引用(也就是一个引用),它保存了DataWrap对象的地址值
System.out.println("交换后a,b成员变量的值:a="+dw.a+",b="+dw.b);
}
private static void swap(DataWrap dw) {
int tmp = dw.a;
dw.a = dw.b;
dw.b = tmp;
System.out.println("swap方法里a,b成员变量的值:a="+ dw.a+",b="+dw.b);
//把dw直接赋值为null,让它不在指向任何有效地址
dw = null;
}
}
2.4 方法重载(overload)
// 方法重载:指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可,与修饰符和返回值类型无关
// 特点:
1)参数列表:个数不同 、数据类型不同 、顺序不同
2)重载方法调用:JVM通过方法的参数列表,调用不同的方法
//方法重载与修饰符和返回值类型无关!!!
public void Info(String s,int a){}
public void Into(int a,String s){} //方法名相同,参数个数形同、参数类型相同、但参数顺序不同
public void Info(String s,String s1){} //方法名相同,参数个数形同、参数类型不同
public void Into(int a,String s,double d){} //方法名相同,参数个数不同
注意:方法修饰符不同,不构成方法的重载。
2.5 可变参数
可变参数
:适用于参数个数不确定,但类型确定的情况,java把可变参数当做数组处理。我们使用...
表示可变长参数,…位于参数类型和参数名之间,前后有无空格都可以。
【示例】
public static void main(String[] args) {
System.out.println(add(1, 2)); // 输出:3
System.out.println(add(1, 2, 3)); // 输出:6
}
// 可变参数的方法
public static int add(int a, int b, int ... arr) {
int sum = a + b;
for(int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
可变参数的特点:
- 一个方法中可变参数最多只能有一个,并且只能出现在参数列表的最后面。
- 调用可变参数的方法时可以给出任意个参数,在方法体中以数组的形式访问可变参数。
三、常量、成员变量、局部变量
3.1 常量
常量;指在Java程序中不变的量
当使用常量的时候,前缀 0 表示 8进制,而前缀 0x 代表16进制
int a = 01;
int b = 0x12;
常量的分类
类型 | 含义 | 举例 |
---|---|---|
整数常量 | 所有的整数 | -20、-1、0、24、101 |
小数常量 | 所有的小数 | 0.1、-2.7、6.66 |
字符常量 | 单引号引起来,只能写一个字符,也可以不写 | ‘a’、‘’、‘牛’ |
字符串常量 | 双引号引起来,可以写多个字符,也可以不写 | “a”、“666”、“你好吗?”、“” |
布尔常量 | 只有两个值:true、false(流程控制中讲解) | true、false |
空常量 | 只有一个值(引用数据类型中讲解) | null |
3.2 成员变量和局部变量的区别
!!成员变量有默认值;局部变量没有默认值必须显示初始化;
!!如果局部变量和成员变量同名,局部变量将覆盖成员变量,如果想要在这个方法里引用被覆盖的成员变量,则可以使用 this(对于实例变量)
或 类名(对于类变量)
作为调用者来限定访问成员变量。
// 变量根据定义位置的不同,我们给变量起了不同的名字
1)在类中的位置不同(重点)
成员变量:类中,方法外
局部变量:方法中或者方法声明上(形式参数)
2)作用范围不一样(重点)
成员变量:类中
局部变量:方法中
3)初始化值的不同(重点)
成员变量:有默认值
局部变量:没有默认值。必须先定义,赋值,最后使用
4)在内存中的位置不同(了解)
成员变量:堆内存
局部变量:栈内存
5)生命周期不同(了解)
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
6)可用修饰符不同
成员变量可以被public、protected、private、static、final等修饰符修饰;
局部变量只能被final修饰。
3.3 成员变量内存图
class Person{
//定义一个实例变量
public String name;
//定义一个类变量(属于类而不属于对象) 类初始化时为eyeNum分配内存
public static int eyeNum;
}
public class PersonTest {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();
p1.name = "张三";
p1.eyeNum = 2;//最好不要使用对象来调用类变量!!!!
p2.name = "孙悟空";
p2.eyeNum = 3;//最好不要使用对象来调用类变量!!!!
//使用类来调用类变量
System.out.println(Person.eyeNum); // 3
}
}
3.4 局部变量内存图
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。
定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才回为局部变量分配内存,并将初始值保存到这块内存中。
⚠️局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
栈内存中的变量无须系统垃圾回收,通常随方法或代码块的运行结束而结束。
3.5 static 关键字
⚠️static 修饰的内容:
- static修饰变量的时候:存在静态区,只有一份,随着类的加载而加载,且只加载一次;
- static修饰方法时:静态方法只能访问静态方法;非静态方法可通过类名.方法来访问静态方法;
- 它优先于对象存在,所以,可以被所有对象共享。
⚠️静态方法调用的注意事项:
- 静态方法只能访问类静态变量和静态方法。
- 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类静态变量或静态方法。
- 静态方法中,不能使用this关键字!!
// 静态变量
// 使用 static关键字修饰的成员变量
static 数据类型 变量名;
static int numberID;
// 静态方法
// 当 static 修饰成员方法时,该方法称为【类方法】,习惯称为【静态方法】。
// 静态方法在声明中有 static ,建议使用类名来调用,而不需要创建类的对象。调用方式非常简单。
修饰符 static 返回值类型 方法名 (参数列表){
// 执行语句
}
// 静态方法示例
public static void showNum() {
System.out.println("num:" + numberOfStudent);
}
调用格式
// 被static修饰的成员可以并且建议通过类名直接访问。虽然也可以通过对象名访问静态成员,原因即多个对象均属于一个类,共享使用同一个静态成员,但是不建议,会出现警告信息
// 格式
// 访问类变量
类名.类变量名;
// 调用静态方法
类名.静态方法名(参数);
// 示例
public class StuDemo2 {
public static void main(String[] args) {
// 访问类变量
System.out.println(Student.numberOfStudent);
// 调用静态方法
Student.showNum();
}
}
四、构造器、静态代码块
4.1 构造器
构造方法
也叫构造器(constructor)
,用于给对象进行初始化操作,即为对象成员变量赋初始值。
声明格式: [修饰符] 类名(形参列表) { }
注意事项:
1、构造方法名必须和类名相同,采用大驼峰命名规范。
2、构造方法不用定义返回值类型,在方法体中不允许return具体的数据。
3、构造方法必须通过new关键字调用,是一种特殊的方法。
4、无论你与否自定义构造方法,所有的类都有构造方法,因为Java自动提供了一个无参数构造方法,一旦自己定义了构造方法, Java自动提供的默认无参数构造方法就会失效。
// 示例
public class Demo01 {
private String name;
private int age;
private long tel;
//无参构造器
public Demo01(){
}
//一个参数的构造器
public Demo01(String name){
this.name = name;
}
//两个参数的构造器 形成构造器重载
public Demo01(String name,int age,long tel){
//this调用另一个重载的构造器;this调用构造器时必须放在构造器的第一条语句!!!
this(name);
this.age = age;
this.tel = tel;
}
}
⚠️注意事项:
1. 如果你不提供构造方法,系统会给出无参数构造方法。
2. 如果你提供了构造方法,系统将不再提供无参数构造方法。
3. 构造方法是可以重载的,既可以定义参数,也可以不定义参数。
4.2 构造方法和成员方法区别
区别一:定义格式区别
- 构造函数的方法名要与类名一样,并且不用定义返回值类型。
- 成员方法的方法名只需符合标识符的规范,必须定义返回值类型。
区别二:调用时期区别
- 构造方法在实例化对象的时候调用。
- 成员方法在对象创建成功之后调用。
区别三:调用方式区别
- 构造方法通过new关键字来调用。
- 成员方法通过对象来调用。
区别四:调用次数区别
- 构造方法只能调用一次,在创建对象的时候调用。
- 成员方法可以调用任意多次!
注意:构造方法中可以调用成员方法,成员方法中不能调用构造方法。
4.2 静态代码块
静态代码块
:定义在成员位置,使用static修饰的代码块{ },即static {}
。
- 位置:类中方法外(不在乎在哪一行,只要是类中方法外就行)
- 执行:随着类的加载而执行且执行一次,优先于main方法和构造方法的执行。
// 格式:
public class ClassName{
static {
// 执行语句
}
}
// 作用:进行初始化赋值
public class Game {
public static int number;
public static ArrayList<String> list;
static {
// 给类变量赋值
number = 2;
list = new ArrayList<String>();
// 添加元素到集合中
list.add("张三");
list.add("李四");
}
}
⚠️static 关键字,可以修饰变量、方法和代码块。在使用的过程中,其主要目的还是想在不创建对象的情况下,去调用方法。
五、包
5.1 为什么需要包?
包机制是java中管理类重要手段,给类提供了多层命名空间。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于,文件夹对于文件的作用。
在java中我们通过package
实现对类的管理,我们将相同功能的类放到一个包中,并且日常项目的分工也是以包作为边界。
注意:同一个包中,不允许有相同的类名。
5.2 如何定义和使用包?
定义包
一般是公司域名反写,再加上模块名,便于内部管理类。可以有多层包,包名采用全部小写字母,多层包之间用“.
”连接。
【示例】包的命名举例
com.java.object
com.java.object.demo
注意:com.java.object 和 com.java.object.demo,这两个包没有包含关系,是两个完全独立的包。只是逻辑上看起来后者是前者的一部分。
使用包
在程序有效代码的第一行(注释不算)声明包。
package com.java.object; // 包的声明,必须在有效代码的第一行(注释不算)
// 以下是业务逻辑所需要的代码
class Person {}
5.3 常用包
java中的常用包 | 说明 |
---|---|
java.lang | 包含一些Java语言的核心类,如String、Math、System等。 |
java.awt | 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。 |
java.net | 包含执行与网络相关的操作的类。 |
java.io | 包含能提供多种输入/输出功能的类。 |
java.util | 包含一些实用工具类,如定义系统特性、与日期日历相关的类。 |
5.4 类的访问
1)类的简化访问
当我们要使用一个类时,这个类与当前程序在同一个包中(即同一个文件夹中),或者这个类是 java.lang 包中的类时通常可以省略掉包名,直接使用该类。
package com.java.object;
class Person {}
public class Test {
public static void main(String[] args) {
// 访问同一包中的类
Person p = new Person();
// 访问java.lang包中的类
String str = "hello world";
}
}
2)类的带包名访问
在访问类时,为了能够找到该类,必须使用含有包名的类的全限定名(包名.类名),例如:java.util.Date。
使用带包的类来创建对象格式:包名.类名 变量名 = new包名.类名();
// 类的带包名访问举例
java.util.Date date = new java.util.Date();
5.5 import 导入类
我们每次使用类时,都需要写很长的包名。很麻烦,我们可以通过 import 导包的方式来简化。可以通过导包的方式使用该类,可以避免使用全类名编写(包类.类名),以便于编写代码,提高可维护性。
导包的格式:import 包名.类名;
import 导包代码书写的位置:在声明包 package 后,定义所有类 class 前。
//【示例】import的使用举例(一)
package com.java.object;
// 导入位置:在声明包package后,定义所有类class前。
import java.util.Date; // 导入日期类
public class Test {
public static void main(String[] args) {
// 使用Date类
Date date = new Date();
}
}
//【示例】import的使用举例(二)
package com.java.object;
// 导入位置:在声明包 package后,定义所有类 class前。
import java.util.*; // 导入该包下所有的类。会降低编译速度,但不会降低运行速度。
class Test {
public static void main(String[] args) {
// 使用java.util包下的类
Date date = new Date();
Scanner s = new Scanner(System.in);
}
}
//注意:如果导入两个同名的类中的一个,只能用包名+类名来显式调用相关类。
//【示例】导入同名类的处理
import java.sql.Date;
import java.util.*;
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
}
}
静态导入(static import)
是在JDK1.5新增加的功能,其作用是用于导入指定类的静态变量或静态方法,这样我们可以直接使用静态变量或静态方法。
// 以下两种静态导入的方式二选一即可
import static java.lang.Math.*; // 导入Math类的所有静态变量和静态方法
import static java.lang.Math.PI; // 导入Math类的PI属性
public class Test {
public static void main(String[] args) {
System.out.println(PI);
System.out.println(random());
}
}
六、封装、隐藏
6.1 访问控制符
6.2 封装概述
封装
可以被认为是一个保护屏障,防止该类的代码和数据被其他类随意访问。要访问该类的数据,必须通过指定的方式。适当的封装可以让代码更容易理解与维护,也加强了代码的安全性。
原则:将属性隐藏起来,若需要访问某个属性,提供公共方法对其访。
/** 封装的步骤 */
1)使用 private 关键字来修饰成员变量
2)对需要访问的成员变量,提供对应的一对 getXxx 方法 、 setXxx 方法
// 示例
public class Student {
// 隐藏属性
private String name;
private int age;
// 提供 getXxx 方法(取值) / setXxx(赋值) 方法,可以访问成员变量
public void setName(String n) {
name = n;
}
public String getName() {
return name;
}
public void setAge(int a) {
age = a;
}
public int getAge() {
return age;
}
}
6.3 this 关键字
⚠️this 关键字总是指向调用该方法的对象。
- 构造器中引用该构造器正在初始化的对象。
- 在方法中引用调用该方法的对象。
!!只有当这个方法被调用时,它所代表的对象才被确定下来:谁在调用这个方法,this就代表谁。
⚠️在构造器中this关键字可以调用另一个构造器,但是必须写在构造器内部第一条语句位置。!!!
public class Demo01 {
private String name;
private int age;
private long tel;
//无参构造器(如果有有参构造器的话无参构造器会被隐藏,如果不显示声明则用不了无参构造器!!!)
public Demo01(){
}
//一个参数的构造器
public Demo01(String name){
this.name = name;
}
//两个参数的构造器 形成构造器重载
public Demo01(String name,int age,long tel){
//this调用另一个重载的构造器;this调用构造器时必须放在构造器的第一条语句!!!
this(name);
this.age = age;
this.tel = tel;
}
}
七、类的继承(extends)
继承(extends)
:就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为,并能拓展新的能力。子类可以直接访问父类中的非私有的属性和行为。
继承的优点:
- 提高代码的复用性。
- 类与类之间产生了关系,是多态的前提。
⚠️继承的注意点:
- java 只支持单继承,不允许多继承,也就是说一个类只能有一个直接父类。
- java 支持多层(重)继承,即一个类的父类可以再去继承另外的父类(继承体系)。
- 如果定义一个类时,没有使用extends,则它的父类默认是:java.lang.Object。
- 只能继承非私有的成员变量或方法!!!
- 子类如果有和父类同名的方法则覆盖父类同名的方法!(相当于重写)调用父类被重写的同名方法:
super.父类方法名
- 子类如果有和父类同名的属性则覆盖父类同名的属性!!调用父类同名变量:
super.父类成员变量名
- 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
- 子类可以继承接口中的默认方法;例如:default void defaultMethod(){}
格式:
// 通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:
class 父类 {
...
}
class 子类 extends 父类 {
...
}
注意:
- 子类又被称为
派生类
,父类又被称为超类(Super Class)
。
7.1 成员变量重名
class Fu{
//父类私有成员
private int id;//私有变量不能被继承
//父类普通成员
String name = "许褚";
//父类中的show方法
public void show(){
System.out.println("Fu--show();");
}
}
class Zi extends Fu{
//子类私有成员
private int id;
//子类与父类同名的成员
String name = "刘备";
//子类与父类同名的show方法(同名则覆盖父类方法)
public void show(){
//调用父类的show方法
super.show(); //Fu--show();
//访问父类中的name
System.out.println("Fu--name="+super.name);//Fu--name=许褚
//访问子类中的name
System.out.println("Zi--name="+this.name);//Zi--name=刘备
System.out.println("Zi--name="+name);//Zi--name=刘备 //默认省略this关键字
}
}
public class ExtendsTest02 {
public static void main(String[] args) {
Zi z = new Zi();
//调用子类的成员变量
System.out.println(z.name);//刘备
//调用子类中的show方法
z.show();
}
}
如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。
7.2 成员方法重名(重写 Override)
如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写 (Override)
。
方法重写
:子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。
class Fu {
public void show() {
System.out.println("Fu show");
}
}
class Zi extends Fu {
//子类重写了父类的show方法
public void show() {
System.out.println("Zi show");
}
}
public class ExtendsDemo05{
public static void main(String[] args) {
Zi z = new Zi();
// 子类中有show方法,只执行重写后的show方法
z.show(); // Zi show
}
}
重写的应用:
子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。
注意:
- 子类方法覆盖父类方法,必须要保证权限大于等于父类权限。
- 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。
7.3 重载和重写(重点)
重载(Overload)
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
最常用的地方就是构造器的重载。
⚠️重载规则:
1.被重载的方法必须改变参数列表(参数个数或类型不一样);
2.被重载的方法可以改变返回类型;
3.被重载的方法可以改变访问修饰符;
4.被重载的方法可以声明新的或更广的检查异常;
5.方法能够在同一个类中或者在一个子类中被重载。
6.无法以返回值类型作为重载函数的区分标准。
重写(Override)
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
⚠️方法的重写规则
1.参数列表与被重写方法的参数列表必须完全相同。
2.返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
3.访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
4.父类的成员方法只能被它的子类重写。
5.声明为 final 的方法不能被重写。
6.声明为 static 的方法不能被重写,但是能够被再次声明。
7.子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
8.子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
9.重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
10.构造方法不能被重写。
11.如果不能继承一个类,则不能重写该类的方法。
7.4 初始化顺序(重点)
class Father{
static{
System.out.println("父类 静态代码块");
}
{
System.out.println("父类 代码块");
}
Father(){
System.out.println("父类 构造器");
}
}
class Son extends Father{
static {
System.out.println("子类 静态代码块");
}
{
System.out.println("子类 代码块");
}
Son(){
System.out.println("子类 构造器");
}
}
public class ExtendsTest03 {
public static void main(String[] args) {
Son s = new Son();
}
}
父类与子类的执行顺序如下
:
父类静态变量 🔽
父类静态代码块 🔽
子类静态变量 🔽
子类静态代码块 🔽
父类非静态变量 🔽
父类非静态代码块 🔽
父类构造函数 🔽
子类非静态变量 🔽
子类非静态代码块 🔽
子类构造函数 🔚
八、多态(polymorphism)
8.1 定义
多态: 是指同一行为,具有多个不同表现形式。多态是封装、继承之后,面向对象的第三大特性 。
多态的必要条件:
- 继承(extends)或实现(implements)是多态的前提。【二选一】
- 子类重写父类方法。【不重写没意义】
- 父类引用指向子类对象。【格式提现】
多态的使用场合:
- 使用父类做方法的形参,实参可以是任意子类类型。
- 使用父类做方法的返回值类型,返回值可以是任意子类的对象。
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
【格式】父类类型 变量名 = new 子类对象;
// 示例
Fu f = new Zi();
【案例】
public abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a1 = new Cat();
// 调用的是 Cat 的 eat
a1.eat();
// 多态形式,创建对象
Animal a2 = new Dog();
// 调用的是 Dog 的 eat
a2.eat();
}
}
⚠️注意
- 多态中若子类中与父类中普通成员变量同名则 执行子类的成员变量(实例变量不具备多态性)。
- 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后法。
8.2 引用类型转换
向上转型
:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。
优点:隐藏了子类类型,提高了代码的扩展性,多态本身就是向上转型的过程。
缺点:只能使用父类共性的内容,不能调用子类特有的方法!
// 示例:当父类引用指向一个子类对象时,便是向上转型
父类类型 变量名 = new 子类类型();
Animal a = new Cat();
向下转型
:父类类型向子类类型向下转换的过程,这个过程是强制的。
优点:向下转型之后,可以调用子类特有的方法。
缺点:向下转型有风险,容易发生ClassCastException异常!
// 示例:一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型
子类类型 变量名 = (子类类型) 父类变量名;
Cat c = (Cat) a;
【案例】
// 父类
class Person {
public void eat() {
System.out.println("person eat ...");
}
}
// 子类
public class Student extends Person {
public void study() {
System.out.println("Student study ...");
}
}
// 测试类
public class PolymorphismDemo {
public static void main(String[] args) {
// 向上转型:父类引用指向子类对象
Person p = new Student();
p.eat(); // 调用Person的eat()方法
// p.study(); // 编译失败,不能调用Student的study()方法
// 向下转型:子类引用指向父类对象
Student stu = (Student)p;
stu.eat(); // 调用Person的eat()方法
stu.study(); // 调用Student的study()方法
}
}
8.3 instanceof 运算符
为了避免ClassCastException的发生,Java提供了 instanceof 关键字
,给引用变量做类型的校验。
格式如下: boolean result = object instanceof class
- 如果变量属于该数据类型,返回 true。
- 如果变量不属于该数据类型,返回 false。
注意:格式中 class可以是类,也可以是接口!
在编译状态中:
class是object对象的父类、自身类、兄弟类和子类时,这四种情况下编译都不会报错。
在运行转态中:
class是object对象的父类和自身类时,返回的结果为true;class是object对象的子类和兄弟类时,返回的结果为false。
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
8.4 多态中成员变量特点
无论编译和运行,都看等号左边(引用类型变量所属的类)。
class Father {
int num = 10;
}
class Son extends Father {
int num = 20;
String name = "小明";
}
public class InstanceDemo {
public static void main(String[] args) {
Father father = new Son();
// 多态中的成员变量:无论编译和运行,都参考等号左边的类
System.out.println("num:" + father.num); // 编译通过,输出父类中的num值
// System.out.println("name:" + father.name); 编译错误
}
}
8.5 多态中成员方法特点
编译看左边,检查等号左边引用变量所属的类是否有该方法。
运行看右边,在执行期间判断引用变量所指向对象的实际类型,然后根据其实际的类型调用其相应的方法,又称为动态绑定
。如果右边对象没有该方法则执行父类中的方法。(接口中只有默认方法可以供实现类调用)
【案例】
class Father {
public void eat() {
System.out.println("父类中的eat方法");
}
}
class Son extends Father {
public void study() {
System.out.println("子类中的show方法");
}
public void eat() {
System.out.println("子类中的eat方法");
}
}
public class InstanceDemo {
public static void main(String[] args) {
Father father = new Son();
father.eat(); // 编译通过,调用子类中的eat方法
// father.study(); 编译错误
}
}