这里会讲到java的面向对象部分:1.类和对象 ;2.类的无参方法和带参方法;3.对象与封装;4.继承;5.多态。有代码,有注释,有颜色标注重点,有详细的步骤。所以对java面向对象部分迷惑的童鞋可以多看看呀,一定会有所收获的!!!
一、类和对象
1.1 什么是对象:
用来描述客观事物的一个实体,有一组属性和方法构成
1.2对象的特征:
属性:对象具有的各种特征
每个对象的每个属性都拥有特定值
例如:张浩和李明的年龄、姓名不一样
方法(能做的事):对象执行的操作
1.3封装:
对象同时具有属性和方法两种特性
对象的属性和方法通常是被封装在一起,共同体现事物的特性,二者相辅相成,不能分割
1.4什么是类:
类是模子,定义对象将会拥有的特征(属性)和行为(方法)
类和对象的关系:
类是多个对像进行综合抽象的结果,是实体对像的概念模型;而一个对象是一个类的实例类是抽象的概念,仅仅是模板。
对象是一个你能看的着,摸得着的具体实体,类是对象的类型。
1.5定义类:
①定义类名
语法:
访问修饰符 class 类名称{
//省略类的内部具体代码
}
注意:
访问修饰符如public、private等是可选的
class是声明类的关键字
按照命名规范,类名首字母大写
示例:
//定义一个“人”类
public class Person{
//省略类的内部具体代码
}
②编写类的属性
对象所拥有的特征在类中表示时称为类的属性
语法:访问修饰符 数据类型 属性名;
注意:访问修饰符是可选的,除了访问修饰符外,其他的语法和声明变量类似
示例:
//创建“人”类,并为“人”类添加相应属性
public class Person{
public String name;
public String gender;
public int age;
}
③编写类的方法
对象执行操作的行为称为类的方法
语法:
访问修饰符 返回类型 方法名称(参数类型 参数名1,参数类型 参数名2,……){
//……省略方法体代码
}
注意:访问修饰符是可选的
返回类型可以是void,在定义方法时,返回类型为void时表明没有返回值,方法体中不必使用“return”关键字返回具体数据,但是可以使用“return”关键字退出方法
返回类型如果不是“void”,那么在方法体中一定要使用”return“关键字返回对应类型的结果,否则程序会出现编译错误
小括号中的“参数数据类型 参数名1,参数数据类型 参数名2,……“称为参数列表
当需要在方法执行的时候为方法传递参数时才需要参数列表,如果不需要传递参数就可以省略,不过小括号不可以省略,传递多个参数时,以英文的逗号进行分隔
示例:
//在“人”类中定义方法,描述人工作的行为
public class Person{ //定义人类
public String name; //姓名
public String gender; //性别
public int age; //年龄
//工作的行为
public void work(){
System.out.println(this.name+"工作理念:干活挣钱有饭吃");
}
}
1.6创建和使用对象:
创建对象:
类中的对象可以调用类中的成员,如属性和方法等
语法:类名 对象名 = new 类名();
注意:new是关键字;左边的类名为对象的数据类型;右边的类名() 称为类的构造方法
示例:
//创建 Person 类的对象
Person person = new Person();
使用对象:
在java中,要引用对象的属性和方法,需要使用“.“
语法:对象名.属性 //引用对象的属性
对象名.方法名() //引用对象的方法
示例:
public static void main(String[] args){
Person person = new Person(); //创建对象
person.name = "韩冰";
person.gender = "女";
person.age = 22;
person.work();
}
1.7面对对象的优点:
提高代码的可重用性
提高代码的安全性和可维护性
与人类的思维习惯一致
二、类的无参方法和带参方法
2.1无参方法:
2.1.1定义类的方法:
语法:
访问修饰符 返回值类型 方法名(){
//方法的主体
}
两种情况:1有返回值 2 无返回值
1.有返回值:
如果方法具有返回值,方法中必须使用关键字return返回该值,要返回的数据类型要和返回值的类型一样
语法:return 表达式;
作用:跳出方法,返回结果
2.无返回值:
如果方法没有返回值,返回值类型为void
无参的方法表示的就是方法名后面的括号中没有任何内容
示例:
public class AutoLion {
String color = "黄色";
public void run() {
System.out.println("正在以0.1米/秒的速度向前奔跑");
}
public void cry() {
String sound = "大声吼叫" ;
}
public String robBall() {
String ball = "球";
return ball;
}
}
2.1.2方法的调用:
语法:对象名.方法名();
注意:方法是个“黑匣子”,完成某个特定的应用程序功能,并返回结果
方法调用:执行方法中包含的语句
本类中方法的调用:
本类中的方法调用,我们直接编写方法名();即可
非本类中方法的调用:
如果调用的是非本类中的方法,需要先创建对象,然后用对象名.方法名(); 调用
方法调用小结:
方法之间允许相互调用,不需要知道方法的具体实现,实现重用,提高效率
2.1.3全局变量和局部变量:
全局变量:声明在类以内方法以外
局部变量:声明在方法以内
含义:变量声明的位置决定变量作用域;变量作用域确定可在程序中按变量名访问该变量的区域
示例:
public class AutoLion{
//变量1,2,3为全局变量 AutoLion类的方法,其他类的方法都可用
变量1类型 变量1;
变量2类型 变量2;
变量3类型 变量3;
public 返回类型 方法1(){
//变量4为局部变量 仅方法1可用
变量4类型 变量4;
}
public 返回类型 方法2(){
//变量5为局部变量 仅方法2可用
变量5类型 变量5;
}
全局变量和局部变量的区别:
1.作用域不同:
局部变量的作用域仅限于定义它的方法
成员变量的作用域在整个类内部都是可见的
2.初始值不同:
java会给全局变量一个初始值
java不会给局部变量赋予初始值
注意:
在同一个方法中,不允许有同名的局部变量
在不同方法中,可以有同名的局部变量
在同一个类中,全局变量和局部变量同名时,局部变量具有更高的优先级
全局变量和局部变量是可以重名,但是在使用的时候按照就近原则
public class Bian{
int a = 10;
public void a(){
int a = 20;
}
public void b(){
System.out.println(a);
}
public static void main(String[] args) {
Bian bian = new Bian();
bian.b(); //结果是10
}
}
public class Bian{
int a;
public void a(){
int a = 20;
}
public void b(){
System.out.println(a);
}
public static void main(String[] args) {
Bian bian = new Bian();
bian.b(); //结果是0
}
}
public class Bian{
int a;
public void a(){
int b;
}
public void b(){
System.out.println(b); //会报错 因为b是局部变量,不会自动给初始值
}
public static void main(String[] args) {
Bian bian = new Bian();
bian.b();
}
}
2.2带参方法:
2.2.1定义带参数的方法:
语法:
<访问修饰符> 1 返回类型2 <方法名>(<形式参数列表>3) {
//方法的主体
}
示例:
public class StudentsBiz {
String[ ] names = new String[30];
public void addName(String name){ //一个形式参数
//增加学生姓名
}
public void showNames() { //显示全部学生姓名
}
调用带参数的方法:
语法:对象名.方法名(参数1, 参数2,……,参数n4)
示例:
public static void main(String[] args) {
StudentsBiz st = new StudentsBiz();
Scanner input = new Scanner(System.in);
for(int i=0;i<5;i++){
System.out.print("请输入学生姓名:");
String newName = input.next();
st.addName(newName); //实参的类型、数量、顺序都要与形参一一对应
}
st.showNames();
}
带多个参数的方法:
public class Student {
public boolean searchName(int start,int end,String name){
String[] names = {"哈哈","呵呵","嘻嘻","嘿嘿","哼哼"};
boolean find = false;
for(int i=start-1;i
find = true;
break;
}
}
return find;
}
public static void main(String[] args) {
Student stu = new Student();
if(stu.searchName(1, 5, "哼哼")){ //如果起始位置是1,结束位置是5,要找的"哼哼"在数组里 //结果就是找到了
System.out.println("找到了");
}else{ //反之没找到
System.out.println("没找到");
}
}
}
2.2.2数组的使用:
public class ArrayTest {
public String max(int[] a){
int max = a[0];
for(int i=0;i
}
}
String isMax = "最大值是:"+max;
return isMax;
}
@Test
public void test01(){
ArrayTest a = new ArrayTest();
int[] b = {34,12,56,76};
String max = a.max(b);
System.out.println(max);
}
}
2.2.3新创一个类型:
//创建一个学生类
public class Student {
int stuId;
String name;
int age;
public void show(){
System.out.println("学号是:"+stuId+"姓名是:"+name+"年龄是:"+age);
}
}
public class TestStudent {
Student[] students = new Student[30]; //创建对象数组,和创建对象一样;可以使用多次
public void addStu(Student student){
for(int i =0;i
students[i] = student;
break;
}
}
}
public void showStu(){
for(int i =0;i
students[i].show();
}
}
}
@Test
public void test01(){
Student stu = new Student();
TestStudent testStudent = new TestStudent();
stu.stuId = 188;
stu.age = 19;
stu.name = "张三";
testStudent.addStu(stu);
testStudent.showStu();
}
}
三、对象与封装
面向对象设计和开发程序的好处:
交流更加流畅;提高设计和开发效率
一个现实世界的问题如何在计算机中描述它们:
1.找出它的种类
2.找出它的属性
3.找出它的行为
类图:
1.作用:用于分析和设计“类”
2.优点:直观、容易理解
3.图像说明:
构造方法:
1.为什么使用构造方法:
由于创建对象使用点的方式给属性赋值相对繁杂,我们可以使用构造方法来给属性进行初始化值
2.作用:初始化值;更简便的赋值
3.语法:访问修饰符 类名(){}
4.分类:
无参构造方法:
public class Cat {
String name;
int health;
public void show(){
System.out.println(name+health);
}
}
public class TestCat {
public static void main(String[] args) {
Cat cat = new Cat();
cat.show(); //此时的结果是 null 0 因为 name和health都没有赋值都是初始值
}
}
有参构造方法:
public class Cat {
String name;
int health;
public void show(){
System.out.println(name+health);
}
public Cat(String a,int b){
this.name = a; //this. 后面跟的是类中上面声明的属性 "="后面的是有参构造方法括号里的
this.health = b;
}
}
public class TestCat {
public static void main(String[] args) {
Cat cat = new Cat("哈哈",18);
cat.show(); //此时的结果是 哈哈 18,因为在上面类中的有参构造方法中让name和health等于了a和b又在创建对象时给a和b赋了值
}
}
注意:
在我们创建对象的时候就会调用这个构造方法
在我们在类中没有写构造方法的时候,系统会默认给我们一个无参构造方法
但是如果类中有了有参的构造方法,则不会自动生成无参构造方法;推荐在写类的时候将有参和无参构造方法都写
public class Cat {
String name;
int health;
public void show(){
System.out.println(name+health);
}
}
//在类中没有有参构造方法时,我们在main方法中创建对象时可以直接创建,此时其实就是无参的构造方法
public class TestCat {
public static void main(String[] args) {
Cat cat = new Cat();
cat.show();
}
}
//当类中有了有参的构造方法时,在创建对象时就需要在括号中添加内容
public class Cat {
String name;
int health;
public void show(){
System.out.println(name+health);
}
public Cat(int a){} //有参的构造方法
}
public class TestCat {
public static void main(String[] args) {
Cat cat = new Cat(); //报错,因为在类中有了有参的创建对象,如果要修改只能在类中创建一个无参的构造方法,或者在括号中加入有参的构造方法括号中同类型的数据
cat.show();
}
}
在我们执行的时候,new 对象时,对象名后面括号(如:Dog dog = new Dog())里面可以有的参数必须符合类中已声明的有参构造方法
当类中有无参和有参构造方法时,我们在创建对象的时候如果想调用两个,在创建对象时,对象名要区分开,不能取同样的名
public class Cat {
String name;
int health;
public void show(){
System.out.println(name+health);
}
public Cat(String a,int b){
this.name = a;
this.health = b;
}
public Cat(){}
}
public class TestCat {
public static void main(String[] args) {
Cat cat1 = new Cat("哈哈",18);
cat1.show(); //结果是 哈哈 18
Cat cat2 = new Cat();
cat2.show(); //结果是 null 0 虽然都是调用的show方法但是由于构造方法不同结果也是不同的
}
}
this:
1.只能用在方法中;this表示当前类的;不写也是默认存在的
2.可以修饰属性(this.属性名)、方法(this.方法名();)、构造方法(无参:this(); 有参:this(参数);)
在使用this调用构造方法的时候必须在构造方法的第一行(构造方法只能在构造方法中调用)
3.子类在继承父类后也可以直接用this来使用父类中的属性和方法(不是调用父类中的方法,因为子类继承了父类,相当于调用本类中的属性和方法,因此当父类中的方法被重写后,调用父类中的方法需要用super)
重载:
1.方法名相同; 2.参数列表不同(个数,顺序,数据类型); 3.在同一个类中;和访问修饰符以及返回值类型无关;4.并不只是构造方法有重载,普通方法也有重载;5.两个构造方法不能同时互相调用(行成死循环)
public void s1(int a,int b){}
public void s1(int c,int d){} //不能形成重载,因为数据类型形同
public void s1(int a,int b){}
public void s2(String a,int b} //不可以重载,因为方法名不同
public void s1(int a,int b){}
public void s1(String a,int b} //可以重载,因为数据类型不同 和实参名无关
public void s1(int a,int b){}
public int s1(int c,int d){return 1} //可以重载,和访问修饰符以及返回值类型无关
为什么需要使用重载:
在使用的时候调用方法或者赋值更加的便捷
static:
使用场景:在共用的情况下使用(试想饮水机和水杯的案列)
static:静态的,是随着类的加载而加载(静态的加载完才加载普通的);可以修饰属性和方法还有代码块,表示公用的
使用 static修饰的属性或者方法,可以使用对象名. 的方式调用;也可以使用类名. 的方式调用(推荐使用)
static修饰的变量也称为静态变量,静态变量和非静态变量的区别是:静态变量被所有对象共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
被static修饰的成员变量属于类,不属于某个对象(也就是说:多个对象访问或修改static修饰的成员变量时,其中一个对象将static成员变量进行了修改,其它的对象的static成员变量值跟着改变,即多个对象共享同一个static成员变量,不是不能改变)
而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
虽然对于静态方法来说没有this,但是我们在非静态方法中能够通过this访问静态方法成员变量。如下:
public class Test {
static int value = 11;
public static void main(String[] args) {
new Test().printValue();
}
private void printValue() {
int value = 22;
System.out.println(this.value);//输出的结果是:11
}
}
这里的this表示的是当前对象,那么通过new Test()来调用printValue的话,当前对象就是通过new Test()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是11。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出11。需要记住的是:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要权限足够)。
方法调用:
普通方法:普通方法是可以调用普通方法的,也可以调用静态方法
静态方法:静态方法可以调用静态方法,不能调用普通方法(因为静态的加载完才加载普通的)
注意:在静态方法中不能使用this关键字
示例:
public class StartPerson {
String name;
int age;
static String country;
public StartPerson(){}
public StartPerson(String name,int age,String country){
this.name = name;
this.age = age;
this.country = country;
}
public void show(){
System.out.println("姓名是:"+name+",年龄是:"+age+",国籍是:"+country);
}
}
@Test
public void test01(){
StartPerson s1 = new StartPerson("刘德华",40,"中国");
StartPerson s2 = new StartPerson("凤姐",30,"中国");
StartPerson s3 = new StartPerson("古天乐",39,"美国");
s1.show();
s2.show();
s3.show();
/*结果是姓名是:刘德华,年龄是:40,国籍是:美国
姓名是:凤姐,年龄是:30,国籍是:美国
姓名是:古天乐,年龄是:39,国籍是:美国
因为国家是static修饰的*/
}
封装:
为什么使用封装:
java 封装,说白了就是将一大坨公共通用的实现逻辑玩意,装到一个盒子里(class),出入口都在这个盒子上。你要用就将这个盒子拿来用,连接出入口,就能用了,不用就可以直接扔,对你代码没什么影响。
对程序员来说,使用封装的目的:
1.偷懒,辛苦一次,后面都能少敲很多代码,增强了代码得复用性
2.简化代码,看起来更容易懂
3.隐藏核心实现逻辑代码,简化外部逻辑,并且不让其他人修改,jar 都这么干
4.一对一,一个功能就只为这个功能服务;避免头发绳子一块用,导致最后一团糟
在编码过程中,给属性赋值的时候,往往会出现一些不符合实际的值,严重一些的情况会影响项目的安全。此时就可以使用封装来解决这样的问题。没有使用封装的时候,用户可以随意给属性赋值
含义:封装其实就是将属性给隐藏起来,不让用户轻易的去访问
public class Student {
int age;
public void show(){
System.out.println("年龄是:"+age);
}
}
public class TestStudent {
public static void main(String[] args) {
Student student = new Student();
student.age = -10;
}
}
封装的步骤:
①将属性私有化 private
/* 用户可能不会输入符合实际的数值
* 为了安全性,选择将属性使用private保护起来
* private:私有的,仅本类可见
*/
private int age;
②编写setter方法,可以针对用户输入的值进行判断
public void setAge(int age){
if(age>0 && age<100){
this.age = age;
}else{
System.out.println("请输入符合实际情况的年龄(0—100),默认18岁");
this.age= 18;
}
}
③编写getter方法,来获取值
public int getAge(){
return age;
}
四、继承
4.1为什么使用继承:
再多个同种类型中有多个重复的代码,就会冗余
可以使用继承避免此种情况出现
4.2概念:
将共同的代码提取出来放到一个单独的类中,让原来的类继承这个新的类
4.3满足条件:
子类和父类是is-a关系
注意:
继承只能是单根继承(同时只能继承一个类)
可以间接的去继承(如a继承b,b继承c,a也就间接继承了c)
4.4关键字:
extends
4.5哪些是不能被继承的:
1.父类的构造方法
2.private 修饰的内容
3.不同包下的 默认修饰符(default)修饰的内容
4.默认修饰符在同包下修饰的内容可以被继承
4.6 super(写法与this相同):
可以调用 父类中的 属性,方法,构造方法
如果调用构造方法,必须在第一行(构造方法只能在构造方法中调用)
4.7 **构造方法执行顺序:
在有子父类的情况下,创建对象(子类)中,如果在子类的构造方法中没有写调用父类的构造方法的话,会默认调用父类的无参构造方法
在子类构造方法中如果没有明示调用父类构造方法的话,则默认调用父类的无参构造方法
在子类构造方法中如果写了调用父类的具体某个构造方法,则调用写的那个构造方法
子类对象在进行实例化前首先调用父类构造方法,再调用子类构造方法实例化子类对象。
实际在子类构造方法中,相当于隐含了一个语句super(),调用父类的无参构造。同时如果父类里没有提供无参构造,那么这个时候就必须使用super(参数)明确指明要调用的父类构造方法。
4.8如何使用继承:
编写父类:
语法:
class Pet{
//公共的属性和方法
}
编写子类,继承父类:
语法:
class Dog extends1 Pet2{
//子类特有的属性和方法
}
class Penguin extends Pet{
}
注意:
继承只能是单继承,只能同时继承一个
示例:
//子类
public class Dog extends Pet {
String strain;
public Dog (){
System.out.println("子类构造方法");
super.setHealth(health);
}
public String getStrain() {
return strain;
}
public void setStrain(String strain) {
this.strain = strain;
}
/*public void show(){
System.out.println("我的名字叫"+name+",健康值是"+health+",和主人的亲密度是"+love+"我是一只"+strain);
}*/
public void show(){
System.out.println("品种是:"+strain);
}
}
//子类
public class Penguin extends Pet {
String gender;
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public void show(){
System.out.println("性别是"+gender);
}
}
//父类
public class Pet {
String name;
int health;
int love;
public Pet(){
System.out.println("父类构造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
public int getLove() {
return love;
}
public void setLove(int love) {
this.love = love;
}
public void show(){
System.out.print("姓名是:"+name+",健康值是:"+health+",好感度是:"+love);
}
}
4.9访问修饰符:
在同一个项目中
4.10方法的重写:
示例1:
比如,定义Father类
1:姓名,吃饭方法,吃窝窝头。
2:定义Son类,继承Father
1:Son类中不定义任何成员,子类创建对象,仍然可以调用吃饭的方法。
2:父类的吃饭的方法,Son不愿吃。Son自己定义了吃饭的方法。
1:此时父类中有一个吃饭的方法,子类中有2个吃饭的方法,只是方法体不一样。
2:一个类中两个方法一模一样,是不允许的。
1:编译运行,执行了子类的方法。
2:使用父类的方法,在子类方法中,使用super.父类方法名。
class Father {
String name;
void eat() {
System.out.println("吃窝窝");
}
}
class Son extends Father {
public void eat() { // 继承可以使得子类增强父类的方法
System.out.println("来俩小菜");
System.out.println("来两杯");
System.out.println("吃香喝辣");
System.out.println("来一根");
}
}
class Demo8 {
public static void main(String[] args) {
Son s = new Son();
//执行子类的方法
s.eat();
}
}
定义:
该现象就叫做重写(覆盖 override)
在继承中,子类可以定义和父类相同的名称并且参数列表一致的方法,将这种函数称之为方法的重写
前提:
必须存在有继承关系
特点:
当子类重写了父类的方法,那么子类的对象如果调用该方法,一定调用的是重写过后的方法,可以通过super关键字进行父类的重写方法的调用
继承可以使得子类增强父类的方法
**细节:
方法名必须相同
参数列表必须相同
子类重写父类的方法的时候,方法的访问权限必须大于等于父类的方法的访问权限否则会编译报错
子类重写父类的方法的时候,返回值类型必须是父类方法的返回值类型或该返回值类型的子类,不能返回比父类更大的数据类型,如子类方法的返回值类型是object
抛出的异常不能大于父类
***示例2:
public void test01(){
Student stu1= new Student("张三",4,55);
Student stu2 = new Student("张三",4,55);
boolean i = (stu1.equals(stu2));
System.out.println(i); //stu1和stu2是同一人但是结果是false 这时候就需要重写equals方法
//等于是在Student 类里写了一个equals方法
}
public boolean equals(Object obj) {
//比较地址值 地址值如果一样别的就不用看了
if(this==obj){
return true;
}
//先判断传进来的obj是否和Student为同一个对象,用instanceof 来判断
if(obj instanceof Student){
Student i = (Student)obj; //如果不一样,强转,类似把int类型的转为double类型
//看obj里面的内容是否和比较的对象内容是否一样,如果一样为true
if(this.id==i.id &&this.name==i.name&&this.score==i.score){
return true;
}
}
return false;
}
abstract:
抽象的,可以修饰方法和类
抽象方法没有方法体(大括号:{}),抽象方法一定要在抽象类中,但是抽象类中不一定有抽象方法
抽象方法需要被子类都继承实现,如果子类没有重写父类中的抽象方法的话则这个类也需要是抽象类,其方法再由其子类重写
抽象类不能被实例化
重点:
final和abstract,private和abstract,static和abstract,这些是不能放在一起的修饰符,因为abstract修饰的方法是必须在其子类中实现(覆盖),才能以多态方式调用,以上修饰符在修饰方法时其子类都覆盖不了这个方法,final是不可以覆盖,private是不能够继承到子类,所以也就不能覆盖,static是可以覆盖的,但是在调用时会调用编译时类型的方法,因为调用的是父类的方法,而父类的方法又是抽象的方法,又不能够调用,所以上的修饰符不能放在一起。
final:
修饰的类不能被其它类继承
修饰的方法不能被重写
修饰的属性不能被修改
***经典例题:
class A{
public A()
{
System.out.println("1.A类的构造方法");
}
{
System.out.println("2.A类的构造块");
}
static{
System.out.println("3.A类的静态方法");
}
}
public class B extends A{
public B()
{
System.out.println("4.B类的构造方法");
}
{
System.out.println("5.B类的构造块");
}
static{
System.out.println("6.B类的静态方法");
}
public static void main(String[] args)
{
System.out.println("7.start......");
new B();
new B();
System.out.println("8.end.....");
}
}
主类中的静态块优先于主方法执行,所以6应该在7前面执行,但是B类继承于A类,所以先执行A类的静态块3,所以进主方法前的执行顺序为:3 6
进主方法后执行7,new B()之后应先执行A的构造方法然后执行B的构造方法,但由于A类和B类均有构造块,构造块又优先于构造方法执行即 2 1(A的构造家族) 5 4(B的构造家族),有多少个对象,构造家族就执行几次,题目中有两个对象 所以执行顺序为:3 6 7 2 1 5 4 2 1 5 4 8(如果main方法重新建一个类执行顺序是7 3 6 2 1 5 4 2 1 5 4 8)
五、多态
5.1什么是多态:
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
比如你是一个酒神,对酒情有独钟。某日回家发现桌上有几个杯子里面都装了白酒,从外面看我们是不可能知道这是些什么酒,只有喝了之后才能够猜出来是何种酒。你一喝,这是剑南春、再喝这是五粮液、再喝这是酒鬼酒….在这里我们可以描述成如下:
酒 a = 剑南春
酒 b = 五粮液
酒 c = 酒鬼酒
…
这里所表现的的就是多态。剑南春、五粮液、酒鬼酒都是酒的子类,我们只是通过酒这一个父类就能够引用不同的子类,这就是多态——我们只有在运行的时候才会知道引用变量所指向的具体实例对象。
诚然,要理解多态我们就必须要明白什么是“向上转型”。在继承中我们简单介绍了向上转型,这里就在啰嗦下:在上面的喝酒例子中,酒(Win)是父类,剑南春(JNC)、五粮液(WLY)、酒鬼酒(JGJ)是子类。我们定义如下代码:
JNC a = new JNC();
对于这个代码我们非常容易理解无非就是实例化了一个剑南春的对象嘛!但是这样呢?
Wine a = new JNC();
在这里我们这样理解,这里定义了一个Wine 类型的a,它指向JNC对象实例。由于JNC是继承与Wine,所以JNC可以自动向上转型为Wine,所以a是可以指向JNC实例对象的。这样做存在一个非常大的好处,在继承中我们知道子类是父类的扩展,它可以提供比父类更加强大的功能,如果我们定义了一个指向子类的父类引用类型,那么它除了能够引用父类的共性外,还可以使用子类强大的功能。
但是向上转型存在一些缺憾,那就是它必定会导致一些方法和属性的丢失,而导致我们不能够获取它们。所以父类类型的引用可以调用父类中定义的所有属性和方法,对于只存在与子类中的方法和属性它就望尘莫及了。
5.2使用:
父类的引用指向子类的实例(对象)
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
对于面向对象而已,多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
5.3语法:
父类名 父类引用 = new 子类类名(); (类似自动类型转换)
//类名 对象名 = new 类名();
Dog dog = new Dog();
Cat cat = new Cat();
Penguin penguin = new Penguin();
//多态
// 父类名 引用 = new 子类类名();
Pet pet = new Dog();
Pet pet = new Cat();
Pet pet = new Penguin();
如果父类和子类中都有同样的方法,那么我们在多态方式调用的时候调用执行的是子类中的方法
那么父类中的方法就不方便同一操作,因此我们可以将方法设置成抽象方法(abstract修饰)
示例:
public class Dog extends Pet{
private String strain;
public String getStrain() {
return strain;
}
public void setStrain(String strain) {
this.strain = strain;
}
public void show(){
System.out.println("品种为:"+strain);
}
public void hospital(){
System.out.println("吃药打针....");
int h=getHealth()+5;
setHealth(h);
}
}
public class Penguin extends Pet{
private String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
/*public void show(){
System.out.println(name+","+health+","+love+","+sex);
}*/
public void show(){
System.out.println(sex);
}
public void hospital(){
System.out.println("吃药睡觉....");
setHealth(getHealth()+3);
}
}
public abstract class Pet {
private String name;
private int health;
private int love;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
public int getLove() {
return love;
}
public void setLove(int love) {
this.love = love;
}
public void show(){
System.out.println(name+","+health+","+love);
}
public abstract void hospital();
}
public class Master {
/**
* 编写一个带dog去医院的方法
*/
/*public void toHospital(Dog dog){
if(dog.getHealth()<70){
dog.hospital();
}else{
System.out.println("狗狗很健康.....");
}
}
public void toHospital(Penguin pen){
if(pen.getHealth()<80){
pen.hospital();
}else{
System.out.println("企鹅很健康....");
}
}
*/
public void toHospital(Pet pet){
if(pet.getHealth()<80){
pet.hospital();
}else{
System.out.println("宠物很健康....");
}
}
}
public class TestPet {
/**
* 多态:
* 同一种事物在不同条件下的不同状态
*/
@Test
public void test03(){
Master master = new Master();
//Pet pet = new Pet(); 错误
//类名 引用 = new 子类();
//编译看左边运行看右边
Pet pet = new Penguin();
pet.setHealth(40);
master.toHospital(pet);
int health = pet.getHealth();
System.out.println(health);
}
多态情况下,关于属性和方法的访问总结:
父子和子类具有相同的成员变量时(静态或非静态),访问的都是父类的成员变量。
子类重写非静态方法,访问子类的方法,子类重写静态方法,还是访问父类的方法
多态:同一个对象(事物),在不同时刻体现出来的不同状态。
***示例:
猫是猫,猫是动物。
水(液体,固体,气态)。
多态的前提:
A:要有继承关系。
B:要有方法重写。
其实没有也是可以的,但是如果没有这个就没有意义。
动物 d = new 猫();
d.show();
动物 d = new 狗();
d.show();
C:要有父类引用指向子类对象。
父 f = new 子();
用代码体现一下多态。
多态中的成员访问特点:
A:成员变量
编译看左边,运行看左边。
B:构造方法
创建子类对象的时候,访问父类的构造方法,对父类的数据进行初始化。
C:成员方法
编译看左边,运行看右边。
D:静态方法
编译看左边,运行看左边。
(静态和类相关,算不上重写,所以,访问还是左边的)
由于成员方法存在方法重写,所以它运行看右边。
***经典例题:
public class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
public class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
public class C extends B{
}
public class D extends B{
}
public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
//运行结果
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D
在这里看结果1、2、3还好理解,从4开始就开始糊涂了,对于4来说为什么输出不是“B and B”呢?
首先我们先看一句话:当超类(父类)对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。这句话对多态进行了一个概括。其实在继承链中对象方法的调用存在一个优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
分析:从上面的程序中我们可以看出A、B、C、D存在如下关系。
首先我们分析5,a2.show(c),a2是A类型的引用变量,所以this就代表了A,a2.show(c),它在A类中找发现没有找到,于是到A的超类中找(super),由于A没有超类(Object除外),所以跳到第三级,也就是this.show((super)O),C的超类有B、A,所以(super)O为B、A,this同样是A,这里在A中找到了show(A obj),同时由于a2是B类的一个引用且B类重写了show(A obj),因此最终会调用子类B类的show(A obj)方法,结果也就是B and A。
方法已经找到了但是我们这里还是存在一点疑问,我们还是来看这句话:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。这我们用一个例子来说明这句话所代表的含义:a2.show(b);
这里a2是引用变量,为A类型,它引用的是B对象,因此按照上面那句话的意思是说有B来决定调用谁的方法,所以a2.show(b)应该要调用B中的show(B obj),产生的结果应该是“B and B”,但是为什么会与前面的运行结果产生差异呢?这里我们忽略了后面那句话“但是这儿被调用的方法必须是在超类中定义过的”,那么show(B obj)在A类中存在吗?根本就不存在!所以这句话在这里不适用?那么难道是这句话错误了?非也!其实这句话还隐含这这句话:它仍然要按照继承链中调用方法的优先级来确认。所以它才会在A类中找到show(A obj),同时由于B重写了该方法所以才会调用B类中的方法,否则就会调用A类中的方法。
所以多态机制遵循的原则概括为:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法,但是它仍然要根据继承链中方法调用的优先级来确认方法,该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
什么叫引用:
1、首先,你要明白什么是变量。变量的实质是一小块内存单元。这一小块内存里存储着变量的值
比如int a = 1。 a就是变量的名名,1就是变量的值。
2、而当变量指向一个对象时,这个变量就被称为引用变量。 比如A a =new A();
a就是引用变量,它指向了一个A对象,也可以说它引用了一个A对象。我们通过操纵这个a来操作A对象。 此时,变量a的值为它所引用对象的地址
六、接口
接口的特性:
接口不可以被实例化
实现类必须实现接口的所有方法(接口中的方法都需要由其子类实现,否则子类需要是抽象类方法再由其子类实现,与抽象类类似)
实现类可以实现多个接口
方法都是默认public abstract修饰的
在接口中声明的变量都是静态常量
接口主要是针对方法而言,约束性更加的宽泛
接口可以被多实现,多个接口之间使用逗号隔开
如果一个类中既有继承又有实现(使用接口叫实现),将继承写在前面,实现写在后面
接口用interface修饰
实现接口用implements
需要满足的条件:has-a
示例:
/*
* 接口中的方法都是默认public abstract 修饰的
* 接口也是abstract修饰的
*
* 接口是不能创建对象的
* 接口中的方法都需要由其子类实现,否则子类需要是抽象类方法再由其子类实现
* 在接口中声明的变量都是静态常量
* 接口可以被多实现,多个接口之间使用逗号隔开
* 如果一个类中既有继承又有实现,将继承写在前面,实现写在后面
*/
public interface MyInterface {
int num = 10;
public void show();
}
interface YouInterface {
public void you();
}
class Y extends A implements MyInterface ,YouInterface{
public void show(){
}
public void you(){
}
}
class A implements MyInterface{
int num = 10;
public void show(){
}
}
为什么使用接口:
一定程度上弥补了继承的缺陷,接口对内容的约束更加的宽泛,针对的是是否有这个东西,主要是来声明标准的
实际过程中,所有方法不可能只由一个人编写
接口就是把具有相同功能,但是本身却没有任何关系的类的功能抽象出来,然后让我们自定义具体的实现。看到一本书这样写到:“接口就是一份契约,由类实现契约。契约中我编写了某些大的方针与前提,而签了约的类可以具体问题具体分析来实现自己的功能”
接口就是个招牌。
比如说你今年放假出去杭州旅游,玩了一上午,你也有点饿了,突然看到前面有个店子,上面挂着KFC,然后你就知道今天中饭有着落了。
KFC就是接口,我们看到了这个接口,就知道这个店会卖炸鸡腿(实现接口)。
那么为神马我们要去定义一个接口涅,这个店可以直接卖炸鸡腿啊(直接写实现方法),是的,这个店可以直接卖炸鸡腿,但没有挂KFC的招牌,我们就不能直接简单粗暴的冲进去叫服务员给两个炸鸡腿了。
接口和抽象类的比较:
不同点:
抽象类只能是继承一个类
接口是可以被多个实现
抽象类中可以有抽象方法和普通方法
接口中只能有抽象方法
相同点:
都表示抽象的,都可以被其他类来实现其中的方法
都不能被实例化
都有抽象方法
七、异常
什么是异常:
异常是指在程序运行过程中所发生的不正常的事件,它会中断正在运行的程序
什么是异常处理:
Java编程语言使用异常处理机制为程序提供了错误处理的能力
为什么使用异常:
代码冗余、程序员要花很多精力去“堵漏洞”、在实际开发中不一定能够将每一个程序的漏洞都想到并使用if判断避免
@Test
public void test01(){
System.out.print("请输入被除数:");
if(tx.hasNextInt()==true){
int num1 = tx.nextInt();
System.out.print("请输入除数:");
int num2 = tx.nextInt();
if(num2==0){
System.out.println("除数不能为0");
}
System.out.println("商是:"+num1/num2);
}else{
System.out.println("请输入整形数值");
}
}
*如何使用:
try catch finally throw throws
try:
将可能出现异常的代码放入try中
catch:
在一段程序中catch可以有多个,来捕获异常类型,当出现了某种异常就会中断异常代码后面的内容转而执行对应的catch中的内容
*finally:
在程序中一定会被执行的内容,除非是遇到了exit()方法
当程序中有return的时候,会先执行finally中的内容,然后执行return
结论:
1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
最终结论:任何执行try 或者catch中的return语句之前,都会先执行finally语句,如果finally存在的话。 如果finally中有return语句,那么程序就return了,所以finally中的return是一定会被return的, 编译器把finally中的return实现为一个warning。
有return的情况下try catch finally的执行顺序:
下面是个测试程序
public class FinallyTest
{
public static void main(String[] args) {
System.out.println(new FinallyTest().test());;
}
static int test()
{
int x = 1;
try
{
x++;
return x;
}
finally
{
++x;
}
}
}
结果是2。
分析:
在try语句中,在执行return语句时,要返回的结果已经准备好了,就在此时,程序转到finally执行了。
在转去之前,try中先把要返回的结果存放到不同于x的局部变量中去,执行完finally之后,在从中取出返回结果,
因此,即使finally中对变量x进行了改变,但是不会影响返回结果。
它应该使用栈保存返回值。
throw:抛出异常
我们直接在程序中写异常信息
public class Student {
private String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) {
/*
* 如果赋值的是男或者女则正常赋值否则给出异常提示
*
*/
if("男".equals(sex) || "女".equals(sex)){
this.sex = sex;
} else{
//抛出异常
try {
throw new Exception("性别赋值不正确");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void show(){
System.out.println("性别是:"+sex);
}
}
throws:声明异常
在程序中可能有的异常,我们在方法名后声明,但是这样并没有真正处理,只是将异常交给调用者去处理了,如果调用者不处理依然会出现异常,除非使用try—catch
@Test
public void test04(){
Demo1 d = new Demo1();
try {
d.show();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void show() throws Exception{ //声明异常
int[] arr = null;
System.out.println(arr[1]);
}
使用try—catch块捕获异常分为三种情况:
正常:try中的代码块没有发生异常,跳过catch并向后执行
出现异常:
try中的代码块发生异常,匹配catch,进入匹配的catch执行其中的代码,执行完跳出try—catch并向后执行
异常类型不匹配:
try中的代码块发生异常,在catch中没有找到匹配的异常,程序中断(除了finally中的代码,try—catch后的代码块也不执行)
*注意:
在使用catch捕获异常的时候,需要将异常的范围从小到大(子类的异常写在上面,父类的异常写在下面;因为父类的异常范围大,写在前面,后面的子类异常就不执行了)
发生异常时按顺序逐个匹配
只执行第一个与异常类型匹配的catch语句
常见的异常类型:
异 常 类 型 |
说 明 |
ArrayIndexOutOfBoundsException |
数组下标越界异常 |
ClassCastException |
类型转换异常 |
NullPointerException |
空指针异常 |
ArithmeticException |
数学异常 |
InputMismatchException |
输入类型不匹配 |
Exception |
异常层次结构的父类 |
ClassNotFoundException |
不能加载所需的类 |
IllegalArgumentException |
方法接收到非法参数 |
NumberFormatException |
数字格式转换异常,如把"abc"转换成数字 |
示例:
import org.junit.Test;
import java.util.*;
public class Demo1 {
Scanner tx = new Scanner(System.in);
/**
* Throwable
* - - -Error : 错误,是人为造成的,必须解决
* - - - Exception : 异常
* - - - 运行时异常:在运行的时候才会出现的异常,可以暂时不解决
*
* - - - 编译时异常:写代码的同时编译时候出现的异常,必须解决了才可以继续向后执行
*
*
*
* 面试题:常见的异常有哪些,并举例
* ArrayIndexOutOfBoundsException 数组下标越界异常
* ClassCastException 类型转换异常
* NullPointerException 空指针异常
* ArithmeticException 数学异常
* InputMismatchException 输入类型不匹配
*
*/
/**
* InputMismatchException 输入类型不匹配
* ArithmeticException 数学异常
*/
@Test
public void test03(){
/*int[] arr = new int[2];
System.out.println(arr[2]);*/
/*String str = "tom";
Object obj = str;
int num = (int)obj;
System.out.println(num);
*/
int[] arr = null;
System.out.println(arr[1]);
}
@Test
public void test02(){
try{
//编写可能出现异常的代码
System.out.print("请输入被除数:");
int num1 = tx.nextInt();
System.out.print("请输入除数:");
int num2 = tx.nextInt();
System.out.println("商是:"+num1/num2);
}catch(InputMismatchException e){ //捕获异常代码,catch可以出现多次
//可以将异常信息打印出来
//1、打印堆栈中的信息
System.out.println("111111");
System.exit(0);
e.printStackTrace();
}catch(ArithmeticException e){
//2、在控制台直接打印异常的信息
String message = e.getMessage();
System.out.println(message);
}catch(Exception e){
e.printStackTrace();
}finally{
System.out.println("这是除了exit方法之外一定会被执行的内容");
}
System.out.println("程序结束");
}
@Test
public void test01(){
System.out.print("请输入被除数:");
if(tx.hasNextInt()==true){
int num1 = tx.nextInt();
System.out.print("请输入除数:");
int num2 = tx.nextInt();
if(num2==0){
System.out.println("除数不能为0");
}
System.out.println("商是:"+num1/num2);
}else{
System.out.println("请输入整形数值");
}
}
}
自定义异常:
声明一个类继承RuntimeException
重写有参无参构造方法
最后调用
public class MyException extends RuntimeException {
public MyException(String message){
super(message);
}
public MyException(){
super();
}
}
调用
public void setSex(String sex) {
/*
* 如果赋值的是男或者女则正常赋值否则给出异常提示
*
*/
if("男".equals(sex) || "女".equals(sex)){
this.sex = sex;
} else{
//抛出异常
try {
throw new MyException("性别赋值不正确~~~");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
日志:
如果项目上线了,出现问题怎么办呢?同城同区的可以安排开发人员去现场查看调试,如果非同城,派遣工作人员就不便,这个时候就可查阅日志,找到原因然后根据原因解决问题
使用步骤:
1:加入 log4j-1.2.17.jar 将光标放在项目名上新建文件夹 lib(为了统一管理,以后有类似文件都放在这个文件夹中)
2.需要将光标点在文件上,然后 Build-path add to build path ,才可以成功的加入到项目中
2:加入日志的配置文件
在src目录下新建文本文件 文件找不到点other搜索file
文件名后缀名是 .properties
将日志文件赋值到log4j.properties文件中
3:使用
在需要记录日志的类下面导包
然后输入下面内容
Logger logger = Logger.getLogger(Demo1.class.getName());//Demo1为当前类名
@Test
public void text06(){
try{
System.out.print("请输入被除数:");
int a = tx.nextInt();
logger.debug("被除數:"+a); //在需要记录的位置下输入
System.out.print("请输入除数:");
int b = tx.nextInt();
logger.debug("除數:"+b);
System.out.println("商是:"+a/b);
logger.debug("商:"+(a/b));
}catch(InputMismatchException e){
e.printStackTrace();
System.out.println("类型");
}catch(ArithmeticException e){
System.out.println(e.getMessage());
System.out.println("数学");
}
}
配置文件说明:
Log4j由三个重要的组件构成:日志信息的优先级,日志信息的输出目的地,
日志信息的输出格式。日志信息的优先级从高到低有ERROR、WARN、 INFO、DEBUG,
分别用来指定这条日志信息的重要程度;日志信息的输出目的地指定了日志将
打印到控制台还是文件中;而输出格式则控制了日志信息的显示内容。
------------------------------------------------
appender 的配置
------------------------------------------------
org.apache.log4j.ConsoleAppender-----------控制台
org.apache.log4j.FileAppender--------------文件
org.apache.log4j.DailyRollingFileAppender--每天产生一个日志文件
org.apache.log4j.RollingFileAppender-------文件大小到达指定尺寸的时候产生一个新的文件
org.apache.log4j.WriterAppender------------将日志信息以流格式发送到任意指定的地方
------------------------------------------------
记录器的配置
-------------------------------------------------
log4j.rootLogger = [ level ] , appenderName, appenderName, …
| |---追加器名
|
|
----日志的级别
------------------------------------------------
layout 的配置
------------------------------------------------
org.apache.log4j.HTMLLayout(以HTML表格形式布局),
org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
---------------------------------------------------------------------------
Log4J采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下:
---------------------------------------------------------------------------
%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
%r 输出自应用启动到输出该log信息耗费的毫秒数
%c 输出所属的类目,通常就是所在类的全名
%t 输出产生该日志事件的线程名
%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
%m 输出代码中指定的消息
好啦。到目前为止,关于java的面向对象部分,基本写的差不多啦,还剩下对象引用
与对象的区别、多态性理解、向上转型和向下转型、栈和堆等关于java面向对象的提高的
知识,会放在另一篇讲,我们另一篇见,帮助到你们的话,可以点赞关注收藏一波O(∩_∩)O~!!!