当前位置: 首页 > backend >正文

面向对象详解和JVM底层内存分析

神速熟悉面向对象

image-20220210105845926

表格结构和类结构

我们在现实生活中,思考问题、发现问题、处理问题,往往都会用“表格”作为工具。实际上,“表格思维”就是一种典型的面向对象思维。

实际上,互联网上所有的数据本质上都是“表格”。我们在这里通过从表格表示数据开始,引入对象和类。大家就会神奇的发现,原来“表格就是对象”。

以公司雇员表为例:

ID姓名岗位基本工资绩效工资入职日期
1001高小一程序员2000009月1日
1002高小二前台500009月2日
1003高小三销售员3000150009月1日
1004高小四财务500009月3日

上面这个雇员表,可以将公司所有员工信息“结构化”、“标准化”,让管理者可以方便的进行统计和管理。

我们也经常将表中的“列”,叫做“字段”,英文中统一叫做“field”。显然,field定义了表的结构。我们可以通过增加新的field(列),让表中所有的行增加数据:

ID姓名岗位基本工资绩效工资入职日期工作地点
1001高小一程序员2000009月1日北京
1002高小二前台500009月2日北京
1003高小三销售员3000150009月1日郑州
1004高小四财务500009月3日上海

面向对象编程中,类对应表的结构(表的field),我们可以定义出“雇员类”:

image-20220210114335720

通过上图,可以看到,雇员类和雇员表的结构完全一样。只不过,雇员类增加了数据的类型而已。

1.表格的动作和类的方法

每个公司的雇员都要有相关的动作。比如:所有雇员每天的工作标准动作有:

  1. 参加晨会,领取当天任务
  2. 午休
  3. 提交工作日志

我们可以在雇员表中将动作信息也包含进去:

ID姓名岗位基本工资绩效工资入职日期工作地点晨会动作说明
1001高小一程序员2000009月1日北京1. 8:30打卡
1002高小二前台500009月2日北京2. 开晨会
1003高小三销售员3000150009月1日郑州3. 9:00结束
1004高小四财务500009月3日上海4. 喊口号:”加油”

新增的列“雇员动作说明”,显然是对所有的雇员都有用,每个雇员都有这个动作。 在类中就是定义成方法:

image-20220210135251474

当然,我们也可以根据需求,为雇员定义多个动作。比如:午休、提交工作日志、领取工资等等。

3.对象对应“表中的行数据”

前面两节,我们主要讲解的是“表结构和类的对应关系”。那么,表中的数据和什么对应呢?

表中的一行一行的数据,都在表结构的约束范围内,大家的结构都是相同的。如下表:

ID姓名岗位基本工资绩效工资入职日期工作地点晨会动作说明
1001高小一程序员2000009月1日北京1. 8:30打卡2. 开晨会3. 9:00结束4. 喊口号:”加油”
1002高小二前台500009月2日北京
1003高小三销售员3000150009月1日郑州
1004高小四财务500009月3日上海

显然,每一行数据都有“姓名”、“基本工资”等“列”,也都有标准的“晨会动作”。在面向对象编程中,下面三句话大家记住:

  1. 表结构对应:类结构
  2. 一行数据对应:一个对象
  3. 表中所有数据对应:这个类的所有对象

因此,上面的四行数据,我们使用四个对象就需要这样表示(假设有对应的构造方法,如下代码是示意,非真实代码):

emp1 = new Employee(ID:1001, name:"高小一", job:"程序员", baseSalary:20000, salary2:0, hiredate:"9月1日", address:"北京");

emp2 = new Employee(ID:1002, name:"高小二", job:"前台", baseSalary:5000, salary2:0, hiredate:"9月2日", address:"北京");

emp3 = new Employee(ID:1003, name:"高小三", job:"销售员", baseSalary:3000, salary2:15000, hiredate:"9月1日", address:"郑州");

emp4 = new Employee(ID:1004, name:"高小四", job:"财务", baseSalary:5000, salary2:0, hiredate:"9月2日", address:"上海");

有的人可能注意到了,创建对象的时候没有传入“晨会动作”这个列,是因为“晨会动作”是一个所有数据都有的标准动作,没必要再重复为每个对象创建“标准晨会动作”。

【注意】:

  1. 本节课中的代码仅是“示意型”,你可以在文档笔记中写,没有必要再开发环境中写并运行。

面向过程和面向对象思想

面向过程和面向对象的区别

面向过程和面向对象都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。

面向过程适合简单、不需要协作的事务,重点关注如何执行。 面向过程时,我们首先思考“怎么按步骤实现?”并将步骤对应成方法,一步一步,最终完成。 这个适合简单任务,不需要过多协作的情况下。比如,如何开车?我们很容易就列出实现步骤:

image-20220210135942380

比如:把大象装冰箱分几步?

image-20220210135958054

但是当我们思考比较复杂的设计任务时,比如“如何造车?”,就会发现列出1234这样的步骤,是不可能的。那是因为,造车太复杂,需要很多协作才能完成。此时面向对象思想就应运而生了。

面向对象(Oriented-Object)思想更契合人的思维模式。我们首先思考的是“怎么设计这个事物?” 比如思考造车,我们就会先思考“车怎么设计?”,而不是“怎么按步骤造车的问题”。这就是思维方式的转变。

比如,我们用面向对象思想思考“如何设计车”:

image-20220210140217120

天然的,我们就会从“车由什么组成”开始思考。发现,车由如下对象组成:

image-20220210140318789

为了协作,我们找轮胎厂完成制造轮胎的步骤,发动机厂完成制造发动机的步骤;这样,发现大家可以同时进行车的制造,最终进行组装,大大提高了效率。但是,具体到轮胎厂的一个流水线操作,仍然是有步骤的,还是离不开执行者、离不开面向过程思维!

因此,面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。 但是,具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。

我们千万不要把面向过程和面向对象对立起来。他们是相辅相成的。面向对象离不开面向过程!

面向对象是“设计者思维”

image-20220210140610529

面向对象是一种“设计者思维”。设计时,先从问题中找名词,然后确立这些名词哪些可以作为类,再根据问题需求确定的类的属性和方法,确定类之间的关系。

设计一款企业管理软件,我们需要进行面向对象分析。写一首诗、一篇文章、一篇小说也需要进行面向对象分析。

因此,面向对象这种思维是任何人都需要学习、任何人都需要掌握的。

·面向对象和面向过程思想的总结

  • 都是解决问题的思维方式,都是代码组织的方式。

  • 面向过程是一种“执行者思维”,解决简单问题可以使用面向过程。

  • 面向对象是一种“设计者思维”,解决复杂、需要协作的问题可以使用面向对象。

  • 面向对象离不开面向过程:

    1. 宏观上:通过面向对象进行整体设计
    2. 微观上:执行和处理数据,仍然是面向过程

实时效果反馈

1. 如下关于面向对象和面向过程的说法,错误的是:

A 面向过程是一种“执行者思维”,解决简单问题可以使用面向过程

B 面向对象是一种“设计者思维”,解决复杂、需要协作的问题可以使用面向对象

C 面向对象离不开面向过程。宏观上,面向对象做整体设计;微观上,面向过程进行执行

D 面向对象和面向过程是对立,二者势不两立!

答案

1=>D

总结

  • 对象说白了也是一种数据结构(对数据的管理模式),将数据和数据的行为放到了一起。
  • 在内存上,对象就是一个内存块,存放了相关的数据集合!
  • 对象的本质就一种数据的组织方式!

对象和类的详解

类可以看做是一个模版,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造?类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。

类:我们叫做class。 对象:我们叫做Object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

总结

  • 类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
  • 类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。

类的定义

做了关于对象的很多介绍,终于进入代码编写阶段。

本节中重点介绍类和对象的基本定义,属性和方法的基本使用方式。

【示例】类的定义方式

// 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
public class Car { 
}
class Tyre { // 一个Java文件可以同时定义多个class
}
class Engine {
}
class Seat {
}

对于一个类来说,有三种成员:属性field、方法method、构造器constructor

image-20220210143938057

属性(field 成员变量)

属性用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体。

在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化

image-20220210144153765

属性定义格式

[ 修饰符] 属性类型 属性名 = [ 默认值] ;

方法

方法用于定义该类或该类实例的行为特征和功能实现。方法是类和对象行为特征的抽象。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。


[修饰符] 方法返回值类型 方法名(形参列表) {// n条语句
}

【示例】编写简单的学生类

image-20220210144359471

public class SxtStu {//属性(成员变量)int id;String sname;int age;    //方法void study(){System.out.println("我正在学习!");}   //构造方法SxtStu(){}
}

实时效果反馈

1. 关于类中,定义属性的说法,错误的是:

A 属性,也称之为:成员变量

B 属性用于定义该类或该类对象包含的数据或者说静态特征

C 属性作用范围是整个类体

D 无论我们是否手动初始化, Java都使用默认的值对属性做其初始化

答案

1=>D

简单内存分析(帮助理解面向对象)

image-20220210144518495

一、内存区域划分

  1. 栈(Stack)
  • 存储当前线程的栈帧(如main()方法的栈帧)
  • 栈帧包含局部变量(s1:0x12)、方法参数(args:null)和操作数栈
  • 方法调用时会创建新栈帧,执行完成自动弹出
  1. 堆(Heap)
  • 存储对象实例(如地址0x12的SxtStu对象)
  • 包含实例属性:id=1001,age=0,sname="高淇"
  • 垃圾回收主要作用于该区域
  1. 方法区(Method Area)
  • 存储类信息:类结构、方法代码(study0/kickball0)
  • 包含常量池:"学习"、"踢球"等字符串常量
  • 存放静态属性和方法(但示例中未展示静态成员)

public class SxtStu {
int id;
int age;
String sname;public void study(){
System.out.println("学习");
}public void kickball(){
System.out.println("踢球");
}public static void main(String[] args) {
SxtStu s1=new SxtStu();
System.out.println(s1.id);
System.out.println(s1.sname);
s1.id = 1001;
s1.sname = "高淇";
System.out.println(s1.id);
}
}

构造方法(构造器 constructor)!!

构造器用于对象的初始化,而不是创建对象!

image-20220210144630190

构造方法是负责初始化(装修),不是建房子

image-20220210144719296

声明格式:

[ 修饰符] 类名( 形参列表){ //n条语句}

构造器4个要点:

  • 构造器通过new关键字调用!!
  • 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用return返回某个值。
  • 如果我们没有定义构造器,则编译器会自动定义一个无参的构造方法。如果已定义则编译器不会自动添加!
  • 构造器的方法名必须和类名一致!

课堂练习

  • 定义一个“点”(Point)类用来表示二维空间中的点(有两个坐标)。要求如下:
  1. 可以生成具有特定坐标的点对象。
  2. 提供可以计算该“点”距另外一点距离的方法。

参考答案

class Point {double x, y;public Point(double _x, double _y) {x = _x;y = _y;}public double getDistance(Point p) {
return Math.sqrt((x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}public static void main(String[ ] args) {
Point p1 = new Point(3.0, 4.0);
Point origin = new Point(0.0, 0.0);
System.out.println(p1.getDistance(origin));
}
}

实时效果反馈

1. 如下关于构造器的说法,错误的是:

A 构造器用于对象的初始化,而不是创建对象!

B 构造器用于创建对象,以及初始化

C 构造器通过new关键字调用

D 构造器的方法名必须和类名一致

答案

1=>B

构造方法的重载

image-20220210145143829

构造方法也是方法。与普通方法一样,构造方法也可以重载。

【示例4-6】构造方法重载(创建不同用户对象)

public class User {int id; // idString name; // 账户名String pwd; // 密码public User() {}public User(int id, String name) {this.id = id;this.name = name;}public User(int id, String name, String pwd) {this.id = id;this.name = name;this.pwd = pwd;}public static void main(String[ ] args) {User u1 = new User();User u2 = new User(101, "高小七");User u3 = new User(100, "高淇", "123456");        }
}

新手雷区

如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。

this.id 表示属性id;id表示形参id

面向对象的内存分析

为了让大家对于面向对象编程有更深入的了解,我们要对程序的执行过程中,内存到底发生了什么变化进行剖析,让大家做到“心中有数”,通过更加形象方式理解程序的执行方式。

老鸟建议

  • 本节课是为了让初学者更深入了解程序底层执行情况,为了完整的体现内存分析流程,会有些新的名词,比如:线程、Class对象。大家暂时可以不求甚解的了解,后期学了这两个概念再回头来看我们这篇内存分析,肯定收获会更大。
  • 明确目标,我们是通过“分析内存”帮助理解,而不是为分析而分析,把问题搞复杂。

JAVA虚拟机内存模型概念

image-20220210145628540

学习内存模型是为了更好理解面向对象

我们前面做过的内存分析过程:

image-20220210145731441

Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。

虚拟机栈(简称:栈)的特点如下:

  1. 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
  2. JVM为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
  3. 栈属于线程私有,不能实现线程间的共享!
  4. 栈的存储特性是“先进后出,后进先出”
  5. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!

堆的特点如下:

  1. 堆用于存储创建好的对象和数组(数组也是对象)
  2. JVM只有一个堆,被所有线程共享
  3. 堆是一个不连续的内存空间,分配灵活,速度慢!
  4. 堆被所有的线程所共享,在堆上的区域,会被垃圾回收器做进一步划分,例如新生代、老年代的划分。

image-20220210145909173

方法区(也是堆)特点如下:

  1. 方法区是JAVA虚拟机规范,可以有不同的实现。

    i. JDK7以前是“永久代”

    ii. JDK7部分去除“永久代”,静态变量、字符串常量池都挪到了堆内存中

    iii. JDK8是“元数据空间”和堆结合起来。

  2. JVM只有一个方法区,被所有线程共享!

  3. 方法区实际也是堆,只是用于存储类、常量相关的信息!

  4. 用来存放程序中永远是不变或唯一的内容。(类信息【Class对象,反射机制中会重点讲授】、静态变量、字符串常量等)

  5. 常量池主要存放常量:如文本字符串、final常量值

实时效果反馈

1. 如下关于java的内存模型,错误的是:

A 栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧

B 栈是一个不连续的内存空间!

C 堆用于存储创建好的对象。

D 方法区实际也是堆,只是用于存储类、常量相关的信息!

答案

1=>B

程序执行过程内存分析

【示例】编写Person类并分析内存

public class Person {String name;int age;public void show(){System.out.println(name);}
public static void main(String[ ] args) {// 创建p1对象Person p1 = new Person();p1.age = 24;p1.name = "张三";p1.show();// 创建p2对象Person p2 = new Person();p2.age = 35;p2.name = "李四";p2.show();Person p3 = p1;Person p4 = p1;p4.age = 80;System.out.println(p1.age);}}

image-20220210150350901

参数传值机制

image-20220210150509267

Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。

· 基本数据类型参数的传值

传递的是值的副本。 副本改变不会影响原件。

· 引用类型参数的传值

传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。

【示例】多个变量指向同一个对象

public class User {int id;     //idString name;    //账户名String pwd;     //密码public User(int id, String name) {this.id = id;this.name = name;}public static void main(String[ ] args) {User u1 = new User(100, "高小七");User u3 = u1;System.out.println(u1.name);u3.name="张三";System.out.println(u1.name);}
}

执行结果如图4-12所示:

image-20220210151954850

垃圾回收机制(Garbage Collection)

Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。

image-20220210152039854

垃圾回收原理和算法

· 内存管理

Java的内存管理很大程度就是:堆中对象的管理,其中包括对象空间的分配和释放。

  • 对象空间的分配:使用new关键字创建对象即可
  • 对象空间的释放:将对象赋值null即可。

· 垃圾回收过程

任何一种垃圾回收算法一般要做两件基本事情:

  1. 发现无用的对象
  2. 回收无用对象占用的内存空间。

垃圾回收机制保证可以将“无用的对象”进行回收。

无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。

实时效果反馈

1. 关于垃圾回收的说法,错误的是:

A Java的内存管理很大程度就是:堆中对象的管理

B 垃圾回收机制保证可以将“无用的对象”进行回收。

C 无用的对象指的就是没有任何变量引用该对象。

D 无用的对象指的就是长时间没有调用的对象。

答案

1=>D

垃圾回收相关算法

  1. 引用计数法

    堆中的每个对象都对应一个引用计数器,当有引用指向这个对象时,引用计数器加1,而当指向该对象的引用失效时(引用变为null),引用计数器减1,最后如果该对象的引用计算器的值为0时,则Java垃圾回收器会认为该对象是无用对象并对其进行回收。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。

    image-20220210152158532

    【示例】循环引用演示

    代码中,s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。

public class Student {String name;Student friend;public static void main(String[ ] args) {Student s1 = new Student();Student s2 = new Student();s1.friend = s2;s2.friend = s1;    s1 = null;s2 = null;}
}

2.引用可达法(根搜索算法)

程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

通用的分代垃圾回收机制(先了解,学到高级可以再看)

image-20220210152848561

分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、永久代。同时,将处于不同状态的对象放到堆中不同的区域。

1. 年轻代

所有新生成的对象首先都是放在Eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。

2. 年老代

在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。

3. 永久代

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。JDK7以前就是“方法区”的一种实现。JDK8以后已经没有“永久代”了,使用metaspace元数据空间和堆替代。

image-20220210153247735

·Minor GC:

用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中。

·Major GC:

用于清理年老代区域

·Full GC:

用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。

JVM调优和Full GC

在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

  1. 年老代(Tenured)被写满
  2. 永久代(Perm)被写满
  3. System.gc()被显式调用
  4. 上一次GC之后Heap的各域分配策略动态变化

实时效果反馈

1. 如下关于分代垃圾回收机制,错误的说法是:

A 分代垃圾回收,是基于这样一个事实:不同的对象的生命周期是不一样的。

B 将对象分为三种状态:年轻代、年老代、永久代

C Full GC用于清理年轻代、年老代区域。JVM调优中,很大的工作就是Full GC的调节。

D Minor GC:用于清理年老代区域。Major GC:用于清理年青代区域

答案

1=>D

开发中容易造成内存泄露的操作

内存泄漏:

指堆内存由于某种原因程序未释放,造成内存浪费,导致运行速度减慢甚至系统崩溃等。

image-20220210153527394

老鸟建议

  • 在实际开发中,经常会造成系统的崩溃。如下这些操作我们应该注意这些使用场景。 请大家学完相关内容后,回头过来温习下面的内容。不要求此处掌握相关细节。

如下四种情况时最容易造成内存泄露的场景,请大家开发时一定注意:

1.创建大量无用对象

比如:大量拼接字符串时,使用了String而不是StringBuilder。

String str = "";
for (int i = 0; i < 10000; i++) {  str += i;   //相当于产生了10000个String对象
}

2. 静态集合类的使用

像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期 和应用程序一致,所有的对象也不能被释放。

3. 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭

IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网 络连接,不使用的时候一定要关闭。

4. 监听器的使用不当

释放对象时,没有删除相应的监听器

其他要点

  1. 程序员无权调用垃圾回收器。
  2. 程序员可以调用System.gc(),该方法只是通知JVM,并不是运行垃圾回收器。尽量少用,会申请启动Full GC,成本高,影响系统性能。
  3. Object对象的finalize方法,是Java提供给程序员用来释放对象或资源的方法,但是尽量少用

本节作业

  1. 垃圾回收过程一般分为两步,是哪两步?
  2. 垃圾回收常见的两种算法是什么?
  3. 堆内存划分成:年轻代、年老代、永久代。垃圾回收器划分成:Minor GC、Major GC、Full GC。这三种垃圾收回器都对应哪些区域?
  4. 对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。这句话对吗?
  5. System.gc()的作用是什么?
     

实时效果反馈

1. for循环时,创建了多少个字符串对象:

String str = "";
for (int i = 0; i < 10000; i++) {  str += i;
}

A 0

B 1

C 10000

D 100

答案

1=>C

this关键字

image-20220210164223611

this的用法:

  • 普通方法中,this总是指向调用该方法的对象。

  • 构造方法中,this总是指向正要初始化的对象。

创建对象的四步:

image-20220210164322090

this的其他要点:

  • this()调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
  • this不能用于static方法中。
  • this是作为普通方法的“隐式参数”,由系统传入到方法中。

【示例】this的用法详解

public class TestThis {int a, b, c;TestThis() {System.out.println("正要初始化一个Hello对象");}TestThis(int a, int b) {// TestThis(); //这样是无法调用构造方法的!this(); // 调用无参的构造方法,并且必须位于第一行!a = a;// 这里都是指的局部变量而不是成员变量
// 这样就区分了成员变量和局部变量. 这种情况占了this使用情况大多数!this.a = a;this.b = b;}TestThis(int a, int b, int c) {this(a, b); // 调用带参的构造方法,并且必须位于第一行!this.c = c;}void sing() {}void eat() {this.sing(); // 调用本类中的sing();System.out.println("你妈妈喊你回家吃饭!");}public static void main(String[ ] args) {TestThis hi = new TestThis(2, 3);hi.eat();}
}

实时效果反馈

1. 关于this关键字的说法,错误的是:

A 普通方法中,this总是指向调用该方法的对象

B 构造方法中,this总是指向正要初始化的对象

C this可以用于static方法中

D this()调用重载的构造方法,避免相同的初始化代码。

答案

1=>C

static 关键字

image-20220210164818477

静态变量(类变量)、静态方法(类方法):static声明的属性或方法。

静态变量/静态方法生命周期和类相同,在整个程序执行期间都有效。它有如下特点:

  1. 为该类的公用变量,属于类,被该类的所有实例共享,在类载入时被初始化。
  2. static变量只有一份。
  3. 一般用“类名.类变量/方法”来调用。
  4. 在static方法中不可直接访问非static的成员。

【示例】static关键字的使用

public class TestStatic {int id; // idString name; // 账户名String pwd; // 密码static String company = "北京尚学堂"; // 公司名称public TestStatic (int id, String name) {this.id = id;this.name = name;}public void login() {System.out.println(name);}public static void printCompany() {// login();//调用非静态成员,编译就会报错System.out.println(company);}public static void main(String[ ] args) {TestStatic u = new TestStatic (101, "高小七");TestStatic .printCompany();TestStatic .company = "北京阿里爷爷";TestStatic .printCompany();}
}

执行结果如图所示:

image-20220210165146330

示例运行时的内存分配图。

image-20220210165259021

实时效果反馈

1. 如下关于static关键字的说法,错误的是:

A 静态变量/静态方法生命周期和类相同,在整个程序执行期间都有效

B static变量只有一份

C 在static方法中不可直接访问非static的成员

D static变量从属于对象,不是从属于类

答案

1=>D

静态初始化块

构造方法用于对象的普通属性初始化。

静态初始化块,用于类的初始化操作,初始化静态属性。

在静态初始化块中不能直接访问非static成员。

注意事项

静态初始化块执行顺序(学完继承再看这里):

  • 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到类的静态初始化块为止。
  • 构造方法执行顺序和上面顺序一样!!

【示例】static静态初始化块

public class TestStatic2 {static String company; //公司名称static {System.out.println("执行类的初始化工作");company = "北京尚学堂";printCompany();}   public static void printCompany(){System.out.println(company);}   public static void main(String[ ] args) {}
}

执行结果如图所示:

image-20220210170118090

变量的分类和作用域

image-20220210171356219

变量有三种类型:局部变量、成员变量(也称为实例变量)和静态变量

局部变量、成员变量、静态变量的核心区别

类型声明位置从属于生命周期(作用域)
局部变量方法或语句块内部方法/语句块从声明处开始,到方法或语句块结束
成员变量(实例变量)类内部,方法外部对象对象创建,成员变量也跟着创建。对象消失,成员变量也跟着消失;
静态变量(类变量)类内部,static修饰类被加载,静态变量就有效;

实时效果反馈

1. 关于变量的类型,错误的说法是:

A 局部变量,从属于方法

B 成员变量,从属于类

C 成员变量,从属于对象

D 静态变量,从属于类

答案

1=>B

包机制(package、import)

包(package)相当于文件夹对于文件的作用。用于管理类、用于解决类的重名问题。

image-20220210172445863

package

package的使用有两个要点:

  1. 通常是类的第一句非注释性语句。

  2. 包名:域名倒着写即可,便于内部管理类。

【示例】package的命名演示

com.oracle.test;
com.itbaizhan.gao.test;
com.itbaizhan.gao.view;
com.itbaizhan.view.model;

注意事项

  • 写项目时都要加包,不要使用默认包。
  • com.gao和com.gao.car,这是两个完全独立的包。只是逻辑上看,后者是前者的一部分。

【示例】package的使用

package com.itbaizhan;
public class Test {public static void main(String[ ] args) {System.out.println("helloworld");}
}

在IDEA项目中新建包

在src目录上单击右键,选择new->package

image-20220210172918421

在package窗口上输入包名即可

image-20220210173240019

即可在src下面看到包:

image-20220210173418521

接下来,我们就可以在包上单击右键,新建类啦

JDK中的主要包

表 JDK中的主要包

Java中的常用包说明
java.lang包含一些Java语言的核心类,如String、Math、Integer、System和Thread。
java.awt包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
java.net包含执行与网络相关的操作的类。
java.io包含能提供多种输入/输出功能的类。
java.util包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。

导入类import

如果要使用其他包的类,需使用import,从而在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。

image-20220210173846351

注意要点

  • Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
  • 如果导入两个同名的类,只能用包名+类名来显示调用相关类:java.util.Date date = new java.util.Date();

静态导入

静态导入(static import): 其作用是用于导入指定类的静态属性和静态方法,这样我们可以直接使用静态属性和静态方法。

【示例】静态导入的使用

package com.itbaizhan;
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性public class Test2{public static void main(String [ ] args){System.out.println(PI);System.out.println(random());}
}

执行结果如图所示:

image-20220210174316694

实时效果反馈

1. 关于package和import,说法错误的是:

A 使用import导入类后,可以直接使用该类

B java.lang包,也需要手动导入,不会被自动导入

C cn.gao和cn.gao.car,这是两个完全独立的包。只是逻辑上看, 后者是前者的一部分。

D 包名:一般是域名倒着写即可,便于内部管理类。

答案

1=>B

面向对象三大特征

本章重点针对面向对象编程的三大特征:继承、封装、多态进行详细的讲解。不要期望,通过本章学习就“搞透面向对象编程”。本章只是面向对象编程的起点,后面所有的章节说白了都是对面向对象这一章的应用。

老鸟建议

建议大家,学习本章,莫停留!学完以后,迅速开展后面的章节。可以这么说,后续章节所有的编程都是“面向对象思想”的应用而已!

继承

image-20220210174815485

继承是面向对象编程的三大特征之一。继承让我们更加容易实现类的扩展。实现代码的重用,不用再重新发明轮子(don’t reinvent wheels)。

继承有两个主要作用:

  1. 代码复用,更加容易实现类的扩展
  2. 方便建模

继承的实现

从英文字面意思理解,extends的意思是“扩展”。子类是父类的扩展。现实世界中的继承无处不在。比如:

image-20220210175051614

上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果新定义一个Student类,发现已经有Person类包含了我们需要的属性和方法,那么Student类只需要继承Person类即可拥有Person类的属性和方法。

【示例】使用extends实现继承

public class Test{public static void main(String[ ] args) {Student s = new Student("高淇",176,"Java");s.rest();s.study();}
}
class Person {String name;int height;public void rest(){System.out.println("休息一会!");}   
}
class Student extends Person {String major; //专业public void study(){System.out.println("在尚学堂,学习Java");}   public Student(String name,int height,String major) {//天然拥有父类的属性this.name = name;this.height = height;this.major = major;}
}

执行结果如图所示:

image-20220210180149793

instanceof 运算符

instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:

【示例】使用instanceof运算符进行类型判断

public class Test{public static void main(String[ ] args) {Student s = new Student("高淇",172,"Java");System.out.println(s instanceof Person);System.out.println(s instanceof Student);}
}

两条语句的输出结果都是true

继承使用要点

  1. 父类也称作超类、基类。 子类:派生类等。
  2. Java中只有单继承,没有像C++那样的多继承。多继承会引起混乱,使得继承链过于复杂,系统难于维护。
  3. Java中类没有多继承,接口有多继承。
  4. 子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)。
  5. 如果定义一个类时,没有调用extends,则它的父类是:java.lang.Object。

实时效果反馈

1. 如下哪个不是面向对象的三大特征

A 继承

B 封装

C 组合

D 多态

2. 如下关于继承的说法,错误的是:

A 子类继承父类,可以得到父类的全部属性和方法 (除了父类的构造方法),但不见得可以直接访问(比如,父类私有的属性和方法)

B 如果定义一个类时,没有调用extends,则它没有父类

C 继承使用extends关键字来实现

D Java中类没有多继承,接口有多继承

答案

1=>C 2=>B

方法重写override

image-20220210180541587

子类重写父类的方法,可以用自身行为替换父类行为。重写是实现多态的必要条件。

方法重写需要符合下面的三个要点:

  1. = =:方法名、形参列表相同。
  2. :返回值类型和声明异常类型,子类小于等于父类。
  3. :访问权限,子类大于等于父类。

【示例】方法重写

package com.itbaizhan.oop;/*** 测试方法的重写*/
public class TestOverride {public static void main(String[ ] args) {Horse h = new Horse();Plane p = new Plane();h.run();h.getVehicle();p.run();}
}class Vehicle {     //交通工具类public void run() {System.out.println("跑....");}public Vehicle getVehicle(){System.out.println("给你一个交通工具!");return null;}}
class Horse extends Vehicle { // 马也是交通工具@Overridepublic void run() {System.out.println("得得得....");}@Overridepublic Horse getVehicle() {return new Horse();}
}class Plane extends Vehicle {@Overridepublic void run() {System.out.println("天上飞....");}
}

实时效果反馈

1. 方法的重写,指的是:

A 重写指的是:一个类内部,多个相同方法名的方法,也称为:重载。

B 子类重写父类的方法,可以用自身行为替换父类行为。

C 重写的单词是:override

D 重写是实现多态的必要条件

答案

1=>A

final关键字

final关键字的作用:

  • 修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。

    final int MAX_SPEED = 120;

  • 修饰方法:该方法不可被子类重写(子类重新定义父类的方法)。但是可以被重载(同一类中方法名相同但参数列表不同的多个方法)!

  • final void study(){}

  • 修饰类: 修饰的类不能被继承。比如:MathString等。

    final class A {}

image-20220210180925181

final修饰类如图所示。

image-20220210181017370

实时效果反馈

1. 关于final的说法,错误的是:

A final修饰变量: 被他修饰的变量不可改变

B final修饰方法:该方法不可被子类重写

C final修饰类: 修饰的类不能被继承

D final修饰方法:该方法不可被重载

答案

1=>D

继承和组合

image-20220210181051823

结婚就是一种组合。两人组合后,可以复用对方的属性和方法!

除了继承,“组合”也能实现代码的复用!“组合”核心是“将父类对象作为子类的属性”。

【示例】之前继承的代码用组合重新实现

public class Test{public static void main(String[ ] args) {Student s = new Student("高淇",172,"Java");s.person.rest();   //s.rest();s.study();}
}class Person {String name;int height;public void rest(){System.out.println("休息一会!");} 
}class Student /*extends Person*/ {Person person = new Person();String major; //专业public Student(String name,int height,String major) {//拥有父类的对象,通过这个对象间接拥有它的属性和方法this.person.name = name;   //this.name = name;this.person.height = height;   //this.height = height;this.person.rest();this.major = major;}public void study(){System.out.println("学习:");person.rest();System.out.println(this.person2.name);
}}

组合比较灵活。继承只能有一个父类,但是组合可以有多个属性。所以,有人声称“组合优于继承,开发中可以不用继承”,但是,不建议大家走极端。

对于is -a关系建议使用继承,has-a关系建议使用组合。

比如:上面的例子,Student is a Person这个逻辑没问题,但是:Student has a Person就有问题了。这时候,显然继承关系比较合适。

再比如:笔记本和芯片的关系显然是has-a关系,使用组合更好。

本节作业

  1. 面向对象的三大特征是什么?(封装、继承、多态)
  2. 继承的两个主要作用是什么?
  3. JAVA中实现继承是哪个关键词?
  4. JAVA中的类继承,是单继承还是多继承?JAVA中其他地方有多继承吗?
  5. 子类继承父类可以获得除父类构造方法之外的所有,但不见得都能使用。找现实中的例子打比喻描述这个现象。
  6. 定义类时,没有使用extends,他的父类是什么?
  7. 组合和继承的关系如何理解?
  8. 方法重写和方法重载什么区别?
  9. final关键字修饰变量、方法、类,都分别代表什么含义?

实时效果反馈

1. 继承和组合的说法,错误的是:

A 继承和组合,都可以实现代码的复用

B “组合”核心是“将父类对象作为子类的属性”。

C 组合任何时候都好于继承,完全可以替换继承!

D 对于is -a关系建议使用继承,has-a关系建议使用组合。

答案

1=>C

Object类详解

image-20220210181625859

所有类都是Object类的子类,也都具备Object类的所有特性。

Object类基本特性

  1. Object类是所有类的父类,所有的Java对象都拥有Object类的属性和方法。
  2. 如果在类的声明中未使用extends,则默认继承Object类。

image-20220210182336575

【示例】Object类

public class Person {...
}
//等价于:
public class Person extends Object {...
}

toString方法

Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:

public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。

【示例】重写toString()方法

class Person {String name;int age;@Overridepublic String toString() {return name+",年龄:"+age;}
}
public class Test {public static void main(String[ ] args) {Person p=new Person();p.age=20;p.name="李东";System.out.println("info:"+p);Test t = new Test();System.out.println(t);}
}

执行结果如图所示:

image-20220210183153778

补:IDEA部分快捷键

IDEA快捷键和相关操作:

  1. 类的结构视图:alt+7

  2. 看类的源码:ctrl+左键

  3. 自动生成构造器、get、set方法、equals等:alt+insert

  4. 查看错误:alt+enter

  5. 快捷输出常见字符串:

    a) main public static void main(String[] args){}

    b) sout System.out.println();

    c) soutm System.out.println(“描述:所在类中的,所在方法”);

实时效果反馈

1. 关于Object类的说法,错误的是:

A 所有类都是Object类的子类,也都具备Object类的所有特性

B 所有的Java对象都拥有Object类的属性和方法

C 如果在类的声明中未使用extends,则默认继承Object类

D 如果在类的声明中未使用extends,则该类没有父类

答案

1=>D

==和equals方法

image-20220210183531639

==代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。

equals()提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。

equals()默认是比较两个对象的hashcode。但,可以根据自己的要求重写equals方法。

【示例】自定义类重写equals()方法

public class TestEquals { public static void main(String[ ] args) {Person p1 = new Person(123,"高淇");Person p2 = new Person(123,"高小七");      System.out.println(p1==p2);   //false,不是同一个对象System.out.println(p1.equals(p2)); //true,id相同则认为两个对象内容相同String s1 = new String("尚学堂");String s2 = new String("尚学堂");System.out.println(s1==s2);         //false, 两个字符串不是同一个对象System.out.println(s1.equals(s2)); //true,  两个字符串内容相同}
}
class Person {int id;String name;public Person(int id,String name) {this.id=id;this.name=name;}public boolean equals(Object obj) {if(obj == null){return false;}else {if(obj instanceof Person) {Person c = (Person)obj;if(c.id==this.id) {return true;}}}return false;}
}

super关键字

image-20220210183911147

  1. super“可以看做”是直接父类对象的引用。可通过super来访问父类中被子类覆盖的方法或属性。

  2. 使用super调用普通方法,语句没有位置限制,可以在子类中随便调用。

  3. 在一个类中,若是构造方法的第一行没有调用super(...)或者this(...); 那么Java默认都会调用super(),含义是调用父类的无参数构造方法。

【示例】super关键字的使用

public class TestSuper01 { public static void main(String[ ] args) {new ChildClass().f();}
}
class FatherClass {public int value;public void f(){value = 100;System.out.println ("FatherClass.value="+value);}
}
class ChildClass extends FatherClass {public int value;
public int age;public void f() {super.f(); //调用父类的普通方法value = 200;System.out.println("ChildClass.value="+value);System.out.println(value);System.out.println(super.value); //调用父类的成员变量}
public void f2() {System.out.println(age);   }}

执行结果如图所示:

image-20220210184041051

继承树追溯

image-20220210184115205

属性/方法查找顺序:(比如:查找变量h)

  • 查找当前类中有没有属性h
  • 依次上溯每个父类,查看每个父类中是否有h,直到Object
  • 如果没找到,则出现编译错误。
  • 上面步骤,只要找到h变量,则这个过程终止。

构造方法调用顺序:

构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。

注:静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。

【示例】继承条件下构造方法的执行过程

public class TestSuper02 { public static void main(String[ ] args) {System.out.println("开始创建一个ChildClass对象......");new ChildClass();}
}
class FatherClass {public FatherClass() {System.out.println("创建FatherClass");}
}
class ChildClass extends FatherClass {public ChildClass() {System.out.println("创建ChildClass");}
}

执行结果如图所示:

image-20220210184724325

封装(encapsulation)!!!!

封装是面向对象三大特征之一。

image-20220210184858886

封装的作用和含义

我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露简单的接口。

我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

编程中封装的具体优点:

  • 提高代码的安全性。
  • 提高代码的复用性。
  • “高内聚”:封装细节,便于修改内部代码,提高可维护性。
  • “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

封装的实现—使用访问控制符

Java是使用访问控制符来控制哪些细节需要封装,哪些细节需要暴露的。

Java中4种访问控制符分别为private、default、protected、public。

访问权限修饰符

修饰符同一个类**同一个包中子类所有类
private
default
protected
public

image-20220210185217979

【注】关于protected的两个细节:

  1. 若父类和子类在同一个包中,子类可访问父类的protected成员,也可访问父类对象的protected成员。
  2. 若子类和父类不在同一个包中,子类可访问父类的protected成员,不能访问父类对象的protected成员。

封装的使用细节

image-20220210185555208

开发中封装的简单规则:

  • 属性一般使用private访问权限。

    属性私有后, 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。

  • 方法:一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。

实时效果反馈

1. 开发中常见的封装规则,错误的说法是:

A 属性一般使用private访问权限。

B 属性私有后, 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的

C 方法:一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰

D 属性一般使用public,方便外部调用

答案

1=>D

【示例】JavaBean的封装演示

public class Person {// 属性一般使用private修饰private String name;private int age;private boolean flag;// 为属性提供public修饰的set/get方法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 boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的return flag;}public void setFlag(boolean flag) {this.flag = flag;}
}

【示例】封装的使用

class Person {private String name;private int age;public Person() {}public Person(String name, int age) {this.name = name;// this.age = age;//构造方法中不能直接赋值,应该调用setAge方法setAge(age);}public void setName(String name) {this.name = name;}public String getName() {return name;}public void setAge(int age) {//在赋值之前先判断年龄是否合法if (age > 130 || age < 0) {this.age = 18;//不合法赋默认值18} else {this.age = age;//合法才能赋值给属性age}}public int getAge() {return age;}@Overridepublic String toString() {return "Person [name=" + name + ", age=" + age + "]";}
}public class Test2 {public static void main(String[ ] args) {Person p1 = new Person();//p1.name = "小红"; //编译错误//p1.age = -45;  //编译错误p1.setName("小红");p1.setAge(-45);System.out.println(p1);Person p2 = new Person("小白", 300);System.out.println(p2);}
}

执行结果:

image-20220210185753537

 

多态(polymorphism)!!

image-20220211105518126

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

【示例】多态和类型转换

class Animal {public void shout() {System.out.println("叫了一声!");}
}
class Dog extends Animal {public void shout() {System.out.println("旺旺旺!");}public void seeDoor() {System.out.println("看门中....");}
}
class Cat extends Animal {public void shout() {System.out.println("喵喵喵喵!");}
}
public class TestPolym {public static void main(String[ ] args) {Animal a1 = new Cat(); // 向上可以自动转型//传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。animalCry(a1);Animal a2 = new Dog();animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。/*编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。* 否则通不过编译器的检查。*/Dog dog = (Dog)a2;//向下需要强制类型转换dog.seeDoor();}// 有了多态,只需要让增加的这个类继承Animal类就可以了。static void animalCry(Animal a) {a.shout();}/* 如果没有多态,我们这里需要写很多重载的方法。* 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。static void animalCry(Dog d) {d.shout();}static void animalCry(Cat c) {c.shout();}*/
}

执行结果所示:

image-20220211105708568

如上示例,给大家展示了多态最为多见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的行为方式。

由此,我们可以看出多态的主要优势是提高了代码的可扩展性。但是多态也有弊端,就是无法调用子类特有的功能,比如,我不能使用父类的引用变量调用Dog类特有的seeDoor()方法。

那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章节所讲的内容:对象的转型。

多态指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

实时效果反馈

1. 关于多态的说法,错误的是:

A 多态指的是同一个方法调用,由于对象不同可能会有不同的行为。

B 多态是方法的多态,不是属性的多态

C 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象

D 属性也有多态

答案

1=>D

对象的转型(casting)

image-20220211105814325

  1. 父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。
  2. 向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型。

【示例】对象的转型

public class TestCasting {public static void main(String[ ] args) {Object obj = new String("北京尚学堂"); // 向上可以自动转型// obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型/* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。* 不然通不过编译器的检查。 */String str = (String) obj; // 向下转型System.out.println(str.charAt(0)); // 位于0索引位置的字符System.out.println(obj == str); // true.他们俩运行时是同一个对象}
}

执行结果如果所示:

image-20220211110047271

在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常ClassCastException。如示例5-14所示。

【示例】类型转换异常

public class TestCasting2 {public static void main(String[ ] args) {Object obj = new String("北京尚学堂");//真实的子类类型是String,但是此处向下转型为StringBufferStringBuffer str = (StringBuffer) obj;System.out.println(str.charAt(0));}
}

执行结果:

image-20220211110416318

为了避免出现这种异常,我们可以使用instanceof运算符进行判断。

【示例】向下转型中使用instanceof

public class TestCasting3 {public static void main(String[ ] args) {Object obj = new String("北京尚学堂");if(obj instanceof String){String str = (String)obj;System.out.println(str.charAt(0));}else if(obj instanceof StringBuffer){StringBuffer str = (StringBuffer) obj;System.out.println(str.charAt(0));}}
}

抽象类

抽象方法和抽象类

image-20220211110633133

· 抽象方法

  1. 使用abstract修饰的方法,没有方法体,只有声明。
  2. 定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

· 抽象类

  1. 包含抽象方法的类就是抽象类。
  2. 通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

【示例】抽象类和抽象方法的基本用法

//抽象类
abstract class Animal {abstract public void shout(); //抽象方法
}
class Dog extends Animal {  //子类必须实现父类的抽象方法,否则编译错误public void shout() {System.out.println("汪汪汪!");}public void seeDoor(){System.out.println("看门中....");}
}
//测试抽象类
public class TestAbstractClass {public static void main(String[ ] args) {Dog a = new Dog();a.shout();a.seeDoor();}
}

抽象类的使用要点:

  1. 有抽象方法的类只能定义成抽象类
  2. 抽象类不能实例化,即不能用new来实例化抽象类。
  3. 抽象类可以包含属性、方法、构造方法。但是构造方法不能用来new实例,只能用来被子类调用。
  4. 抽象类只能用来被继承。
  5. 抽象方法必须被子类实现。

实时效果反馈

1. 关于抽象类和抽象方法,说法错误的是:

A 有抽象方法的类只能定义成抽象类

B 抽象类能实例化,也能用new来实例化抽象类

C 抽象类可以包含属性、方法、构造方法。

D 抽象方法必须被子类实现

答案

1=>B

接口interface

接口就是一组规范(就像我们人间的法律一样),所有实现类都要遵守。

image-20220211112827602

面向对象的精髓,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

接口的作用

· 为什么需要接口?接口和抽象类的区别?

接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离

接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。

接口和实现类不是父子关系,是实现规则的关系。比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。

如何定义和使用接口

声明格式:

[访问修饰符] interface 接口名  [extends 父接口1,父接口2…]  {
常量定义;   
方法定义;
}

定义接口的详细说明:

  • 访问修饰符:只能是public或默认。
  • 接口名:和类名采用相同命名机制。
  • extends:接口可以多继承。
  • 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
  • 方法:接口中的方法只能是:public abstract。 省略的话,也是public abstract。

要点

  • 子类通过implements来实现接口中的规范。
  • 接口不能创建实例,但是可用于声明引用变量类型。
  • 一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
  • JDK1.8(不含8)之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。

JDK1.8(含8)后,接口中可以包含普通的静态方法、默认方法。

【示例】接口的使用

public class TestInterface {public static void main(String[ ] args) {Volant volant = new Angel();volant.fly();System.out.println(Volant.FLY_HIGHT);Honest honest = new GoodMan();honest.helpOther();}
}
/**飞行接口*/
interface Volant { int FLY_HIGHT = 100; // 总是:public static final类型的;void fly();  //总是:public abstract void fly();
}
/**善良接口*/
interface Honest { void helpOther();
}
/**Angel类实现飞行接口和善良接口*/
class Angel implements Volant, Honest{public void fly() {System.out.println("我是天使,飞起来啦!");}public void helpOther() {System.out.println("扶老奶奶过马路!");}
}
class GoodMan implements Honest {public void helpOther() {System.out.println("扶老奶奶过马路!");} 
}
class BirdMan implements Volant {public void fly() {System.out.println("我是鸟人,正在飞!");}
}

接口中定义静态方法和默认方法(JDK8)

JAVA8之前,接口里的方法要求全部是抽象方法。

JAVA8(含8)之后,以后允许在接口里定义默认方法和静态方法。

image-20220211113226313

image-20220211113248315

JDK8新特性_默认方法

Java 8及以上新版本,允许给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可,这个特征又叫做默认方法(也称为扩展方法)。

默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。作为替代方式,接口可以提供默认方法的实现,所有这个接口的实现类都可以得到默认方法。

课堂代码

public class Test {public static void main(String[] args) {A a = new Test_A();a.moren();}
}interface A {default void moren(){System.out.println("我是接口A中的默认方法!");}
}class Test_A implements A {@Overridepublic void moren() {System.out.println("Test_A.moren");}
}

运行结果:

JDK8新特性_静态方法

image-20220211113445521

JAVA8以后,我们也可以在接口中直接定义静态方法的实现。这个静态方法直接从属于接口(接口也是类,一种特殊的类),可以通过接口名调用。

如果子类中定义了相同名字的静态方法,那就是完全不同的方法了,直接从属于子类。可以通过子类名直接调用。

public class Test {public static void main(String[] args) {A.staticMethod();Test_A.staticMethod();}
}
interface A {public static void staticMethod(){System.out.println("A.staticMethod");}}
class Test_A implements A {public static void staticMethod(){System.out.println("Test_A.staticMethod");}
}

 

静态方法和默认方法

本接口的默认方法中可以调用静态方法。

public class Test {public static void main(String[] args) {A a = new Test_A();a.moren();}
}interface A {public static void staticMethod(){System.out.println("A.staticMethod");}public default void moren(){staticMethod();System.out.println("A.moren");}}class Test_A implements A {public static void staticMethod(){System.out.println("Test_A.staticMethod");}
}

接口的多继承

接口支持多继承。和类的继承类似,子接口extends父接口,会获得父接口中的一切。

image-20220211113822693

【示例】接口的多继承

interface A {void testa();
}
interface B {void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {void testc();
}
public class Test implements C {public void testc() {
}public void testa() {}public void testb() {}
}

老鸟建议

接口语法本身非常简单,但是如何真正使用?这才是大学问。我们需要后面在项目中反复使用,大家才能体会到。 学到此处,能了解基本概念,熟悉基本语法,就是“好学生”了。

字符串String类详解

String是最常用的类,要掌握String类常见的方法,它底层实现也需要掌握好,不然在工作开发中很容易犯错。

image-20220211114433865

  • String类又称作不可变字符序列。

  • String位于java.lang包中,Java程序默认导入java.lang包下的所有类。

  • Java字符串就是Unicode字符序列,例如字符串“Java”就是4个Unicode字符’J’、’a’、’v’、’a’组成的。

  • Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义的类String,每个用双引号括起来的字符串都是String类的一个实例。

【示例】String类的简单使用

String e = ""  ; // 空字符串
String greeting = " Hello World ";

【示例】"+"连接符

int age = 18;
String str = "age is" + age; //str赋值为"age is 18"
//这种特性通常被用在输出语句中:
System.out.println("age  is" + age);

String类和常量池

image-20220211141838390

Java内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:

1. 全局字符串常量池
2. class文件常量池
3. 运行时常量池(Runtime Constant Pool)

我们只关注运行时常量池即可。

【示例】字符串相等判断(以后一般判断字符串值是否相等,使用equals()

String g1 = "北京尚学堂";
String g2 = "北京尚学堂";
String g3 = new String("北京尚学堂");System.out.println(g1 == g2);  // true
System.out.println(g1 == g3);  // false
System.out.println(g1.equals(g3));  //true

实时效果反馈

1. 如下代码的说法,正确的是:

String a = "百战程序员";
String b = new String( "百战程序员");

A a和b是同一个对象

B a和b不是一个对象

a==b返回的值是true

a.equals(b)返回的值是false

答案

1=>B

阅读API文档

· 如何下载API文档

  1. 下载地址,点击进入:

https://www.oracle.com/java/technologies/javase-jdk8-doc-downloads.html

· 查看API文档

下载成功后,解压下载的压缩文件,点击进入docs/api下的index.html文件即可。

· API文档如何阅读

image-20220211142005705

String类常用的方法

String类是我们最常使用的类。列出常用的方法,请大家熟悉。

String类的常用方法列表

方法解释说明
char charAt(int index)返回字符串中第index个字符
boolean equals(String other)如果字符串与other相等,返回true;否则,返回false。
boolean equalsIgnoreCase(String other)如果字符串与other相等(忽略大小写),则返回true;否则,返回false。
int indexOf(String str)返回从头开始查找第一个子字符串str在字符串中的索引位置。如果未找到子字符串str,则返回-1。
lastIndexOf()返回从末尾开始查找第一个子字符串str在字符串中的索引位置。如果未找到子字符串str,则返回-1。
int length()返回字符串的长度。
String replace(char oldChar,char newChar)返回一个新串,它是通过用 newChar 替换此字符串中出现的所有oldChar而生成的。
boolean startsWith(String prefix)如果字符串以prefix开始,则返回true。
boolean endsWith(String prefix)如果字符串以prefix结尾,则返回true。
String substring(int beginIndex)返回一个新字符串,该串包含从原始字符串beginIndex到串尾。
String substring(int beginIndex,int endIndex)返回一个新字符串,该串包含从原始字符串beginIndex到串尾或endIndex-1的所有字符。
String toLowerCase()返回一个新字符串,该串将原始字符串中的所有大写字母改成小写字母。
String toUpperCase()返回一个新字符串,该串将原始字符串中的所有小写字母改成大写字母。
String trim()返回一个新字符串,该串删除了原始字符串头部和尾部的空格。

【示例】String类常用方法一

public class StringTest1 {public static void main(String[ ] args) {String s1 = "core Java";String s2 = "Core Java";System.out.println(s1.charAt(3));//提取下标为3的字符System.out.println(s2.length());//字符串的长度System.out.println(s1.equals(s2));//比较两个字符串是否相等System.out.println(s1.equalsIgnoreCase(s2));//比较两个字符串(忽略大小写)System.out.println(s1.indexOf("Java"));//字符串s1中是否包含JavaSystem.out.println(s1.indexOf("apple"));//字符串s1中是否包含appleString s = s1.replace(' ', '&');//将s1中的空格替换成&System.out.println("result is :" + s);}
}

执行结果如图所示:

image-20220211142248907

【示例】String类常用方法二

public class StringTest2 {public static void main(String[ ] args) {String s = "";String s1 = "How are you?";System.out.println(s1.startsWith("How"));//是否以How开头System.out.println(s1.endsWith("you"));//是否以you结尾s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止System.out.println(s);s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7System.out.println(s);s = s1.toLowerCase();//转小写System.out.println(s);s = s1.toUpperCase();//转大写System.out.println(s);String s2 = "  How old are you!! ";s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除System.out.println(s);System.out.println(s2);//因为String是不可变字符串,所以s2不变}
}

执行结果如图所示:

image-20220211142426612

字符串相等的判断

  • equals方法用来检测两个字符串内容是否相等。如果字符串s和t内容相等,则s.equals(t)返回true,否则返回false。
  • 要测试两个字符串除了大小写区别外是否是相等的,需要使用equalsIgnoreCase方法。
  • 判断字符串是否相等不要使用==

【示例】忽略大小写的字符串比较

"Hello".equalsIgnoreCase("hellO");//true

【示例】字符串的比较:= =equals()方法

public class TestStringEquals {public static void main(String[ ] args) {String g1 = "北京尚学堂";String g2 = "北京尚学堂";String g3 = new String("北京尚学堂");System.out.println(g1 == g2); // true  指向同样的字符串常量对象System.out.println(g1 == g3); // false  g3是新创建的对象System.out.println(g1.equals(g3)); // true  g1和g3里面的字符串内容是一样的}
}

执行结果如图5-33所示:

image-20220211142824723

示例内存分析如图所示:

image-20220211142936764

内部类

我们把一个类放在另一个类的内部定义,称为内部类(inner class)。

image-20220211143001857

内部类的两个要点:

  • 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
  • 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。但外部类不能访问内部类的内部属性。

注意

内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。

【示例】内部类的定义和使用

 

/**外部类Outer*/
class Outer {private int age = 10;public void show(){System.out.println(age);//10}/**内部类Inner*/public class Inner {//内部类中可以声明与外部类同名的属性与方法private int age = 20;public void show(){System.out.println(age);//20}}
}

编译后会产生两个不同的字节码文件,如图所示:

image-20220211143213384

内部类的分类

非静态内部类

非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)

  1. 非静态内部类对象必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部类对象。非静态内部类对象单独属于外部类的某个对象。

  2. 非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。

  3. 非静态内部类不能有静态方法、静态属性和静态初始化块。

  4. 成员变量访问要点:

    • 内部类属性:this.变量名。

    • 外部类属性:外部类名.this.变量名。

【示例】内部类中访问成员变量

 

/**外部类Outer1*/
class Outer1 {private int age = 10;public void show(){System.out.println(age);//10}/**内部类Inner*/public class Inner1 {//内部类中可以声明与外部类同名的属性与方法private int age = 20;public void show(){System.out.println(age);//20System.out.println(Outer1.this.age); //10  访问外部类的普通属性}}
}

内部类的访问:

\1. 外部类中定义内部类:new Inner()。

\2. 外部类以外的地方使用非静态内部类:

Outer.Inner varname = new Outer().new Inner()。

【示例】内部类的访问

/*** 测试非静态内部类*/
public class TestInnerClass1 {public static void main(String[ ] args) {//先创建外部类实例,然后使用该外部类实例创建内部类实例Outer1.Inner1 inner = new Outer1().new Inner1();inner.show();}
}

静态内部类

定义方式:

static class  ClassName {
//类体
}

使用要点:

  1. 静态内部类可以访问外部类的静态成员,不能访问外部类的普通成员。
  2. 静态内部类看做外部类的一个静态成员。

【示例】静态内部类的访问

/*
测试静态内部类*/
class Outer2{private int a = 10;private static int b = 20;//相当于外部类的一个静态成员static class Inner2{public void test(){
//    System.out.println(a);  //静态内部类不能访问外部类的普通属性System.out.println(b); //静态内部类可以访问外部类的静态属性}}
}
public class TestStaticInnerClass {public static void main(String[ ] args) {//通过 new 外部类名.内部类名() 来创建内部类对象Outer2.Inner2 inner =new Outer2.Inner2();inner.test();}
}

匿名内部类

适合那种只需要使用一次的类。比如:键盘监听操作等等。在安卓开发、awt、swing开发中常见。

语法:

new 父类构造器(实参类表) \实现接口 () {//匿名内部类类体!
}

【示例】匿名内部类的使用

/*** 测试匿名内部类*/
public class TestAnonymousInnerClass {public void test1(A a) {a.run();}public static void main(String[] args) {TestAnonymousInnerClass tac = new TestAnonymousInnerClass();tac.test1(new A() {@Overridepublic void run() {System.out.println("匿名内部类测试!  我是新定义的第一个匿名内部类!");}});tac.test1(new A() {@Overridepublic void run() {System.out.println("我是新定义的第二个匿名内部类");}});}}interface A {void run();
}

注意

  • 匿名内部类没有访问修饰符。
  • 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。

 

局部内部类

定义在方法内部的,作用域只限于本方法,称为局部内部类。

局部内部类在实际开发中应用很少。

【示例】方法中的内部类

/*** 测试局部内部类*/
public class TestLocalInnerClass {public void show() {//作用域仅限于该方法class Inner3 {public void fun() {System.out.println("helloworld");}}new Inner3().fun();}public static void main(String[ ] args) {new TestLocalInnerClass().show();}
}

http://www.xdnf.cn/news/6851.html

相关文章:

  • 实验七 基于Python的数字图像水印算法
  • Captiks无线惯性动捕及步态分析系统:高频采样+400g超宽动态量程,赋能医疗康复、竞技体育、工业检测三大领域,运动轨迹零盲区追踪!”
  • linux-软件的安装与部署、web应用部署到阿里云
  • 使用glsl 来做视频矫正
  • wifiactivity 界面 要在哪里注册,注销广播?onResume() vs onPause() 的本质区别
  • 电动调节V型球阀:行业应用与材质选择全解析
  • CSS高级技巧
  • 【MySQL】架构和存储引擎
  • 3D个人简历网站 5.天空、鸟、飞机
  • OpenCV 光流估计:从原理到实战
  • mysql中limit深度分页详细剖析【爽文】
  • 黑马点评-用户登录
  • 边缘计算:物联网的“加速器”与“守护者”
  • CentOS 上配置 Docker 使用 NVIDIA GPU
  • 【论文阅读】人脸修复(face restoration ) 不同先验代表算法整理2
  • Python标准库完全指南:os、sys与math模块详解与实战应用
  • DeepSeek指令微调与强化学习对齐:从SFT到RLHF
  • 【MySQL进阶】如何在ubuntu下安装MySQL数据库
  • React中useState中更新是同步的还是异步的?
  • 配置git从公网能访问-基于frp
  • Oracle 的 ASSM 表空间
  • 【论文阅读】人脸修复(face restoration ) 不同先验代表算法整理
  • 06、基础入门-SpringBoot-依赖管理特性
  • Linux之Nginx安装及配置原理篇(一)
  • 设计模式 - 单例模式 - Tips
  • 面试真题 - 高并发场景下Nginx如何优化
  • 开源安全大模型Foundation-Sec-8B实操
  • C语言_动态内存管理
  • 18.自动化生成知识图谱的多维度质量评估方法论
  • (9)python开发经验