1 面向对象
1.1 面向对象概述
1.1.1 函数封装
函数/方法
中封装的是具体实现某一功能的代码,而通过书写一个拥有多个特定函数的类
,来存放的就是一个又一个的方法
。
方法
都存放在类
里面,当需要使用的时候,不用去找具体的方法,而是先找这个类
,那么就自然而然可以找到其中封装
的方法。
将对数组操作的一些方法,全部封装到ArrayTools中,通过以下代码感受一下封装
的好处。
// 自定义int类型数组
public class ArrayTools {
// 设置属性 外部性质描述
private int[] arr;// 元数组
private int arrLength;// 有效数据的长度
// 行为进行描述
// 创建默认大小的数组
public ArrayTools() {
arr = new int[50];
}
// 创建指定的数组
public ArrayTools(int maxsize) {
arr = new int[maxsize];
}
// 添加新元素
public void insert(int target) {
arr[arrLength] = target;
arrLength++;
}
// 展示数组
public void display() {
System.out.print("[ ");
for (int i = 0; i < arrLength; i++) {
System.out.print(arr[i] + " ");
}
System.out.print("]");
}
// 寻找目标元素位置
public int search(int target) {
int i;
for (i = 0; i < arrLength; i++) {
if (target == arr[i]) {
break;
}
}
if (i == arrLength) {
return -1;
}
return i;
}
// 获取指定位置元素
public int get(int index) {
if (index >= arrLength || index < 0) {
throw new ArrayIndexOutOfBoundsException();
} else {
return arr[index];
}
}
// 删除指定元素
public void delete(int index) {
if (index >= arrLength || index < 0) {
throw new ArrayIndexOutOfBoundsException();
} else {
for (int i = index; i < arrLength; i++) {
arr[i] = arr[i + 1];
display();
}
arrLength--;
}
}
}
方法 :是用于封装功能代码,类用于封装具有某一类相似特性的功能。
面对需求 : 先找Java有没有提供相应的类可以完成此类需求。若没有就自己写个类将功能封装在其中。
1.1.2 理解什么是面向过程
面向过程
也是解决问题的一种思想,当我们在解决问题时,会按照预先设定的想法和步骤,一步一步去实现,而具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,完成我们的需求。
通过上面简单的描述发现面向过程
,其实就是面向着具体的每一个步骤和过程,就是面对具体的每一个功能函数。这些功能函数相互调用,完成需求。
面向过程的代表语言:C语言
。
当需求单一,或者简单时,我们一步一步去操作没问题,并且发现效率也挺高。可随着需求的更改,功能的增多,发现需要面对的每一个步骤已经忙不过来,这时就开始思索,能不能把这些步骤和功能在进行封装
,封装
时根据不同的功能,进行不同的封装
,功能类似的封装在一起。在使用时只要找到封装功能的那个实体即可。这时发现我们从面对具体的一个一个方法过度面向一个具体封装某一类功能的一个个体**。
1.1.3 理解什么是面向对象
当不再面对具体的每一个方法时,发现操作也变的简单了很多。而封装
具体功能的这个类
,才是我们需要面对的。而基于这个封装
了具体功能的类
,一般情况下,在Java中需要通过创建这个类
的实体来使用。
这个实体称之为对象。在开发中,我们是在不断的找封装
不同功能的类
。基于这些类
创建其对象
,使用这些对象完成相应的操作。
通过上面的讲解和分析得出:面向对象
是基于面向过程
,对象
是将功能进行了封装
。只要找到了具体的类
,创建出对象
,就可以调用其中的具体功能。面向对象
也是用来解决问题的一种思维模式。
在以后开发中,先找对象
,调用对象
中的具体功能。如果真的没有能够完成需求的对象
,这时就自己创建对象
,并将所需的功能定义到对象
中,方便以后使用。
面向对象(Object Oriented)是软件开发方法,一种编程范式。面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。
OOP(Object Oriented Design):面向对象编程设计。
1.2.、面向对象和面向过程差异(面向对象的好处)
对面向过程
和面向对象
做出总结:
面向对象
是一种更符合人们思考习惯的思想。面向过程
中更多的体现的是执行者,面向对象
中更多的体现是指挥者。指挥对象做事情。面向对象
将复杂的问题简单化。
总结:
- 先按照
名词提炼
问题领域中的对象
。 - 对
对象
进行描述,其实就是在明确对象
中应该具备的属性
和功能
。 - 通过
new关键字
就可以创建该事物的具体对象
。 - 通过该
对象
调用它以后的功能。使用.
访问属性
和调用方法()
如: 大象.鼻子
2、类与对象
2.1、类与对象
面向对象程序设计
(OOP)面向对象进行程序设计。对象
(object)代表现实世界中可以明确标识的一个实体。例如:一名学生、一张桌子、一个圆、一个按钮甚至一笔贷款都**可以当做是一个对象
**。每个对象
都有自己独特的标识
、状态
和行为
。
- 一个
对象
的状态(state,也称为特征(property))是由具有当前值的属性(数据域)
来表示。例如:一个圆有半径radius的属性,一个矩形有width和height的属性。 - 一个
对象
的行为(behavior,也称动作(action))是由方法定义的。调用对象
的一个方法就是按要求完成一个动作。例如:可以给圆对象定义一个==getArea()和getPerimeter()的方法。分别获取圆的面积和周长。甚至可以调用setradius(radius)==来修改圆的半径。
使用一个通用类
来定义同一类型的对象
,类
是一个模板。用来定义对象的属性
是什么以及方法做什么。一个对象
是一个类
的实例
。可以从一个类
里创建多个实例
。创建实例
的过程称之为:实例化
。对象
和实例
经常可以互换。就像苹果派配方和苹果派的。看下面这个类Circle。
public class Circle {
public final double PI = Math.PI;
// 半径,默认值为1。
double radius = 1;
// 构造方法1
public Circle(double radius) {
this.radius = radius;
}
// 构造方法2
public Circle() {
}
this.radius = radius;
}
public double getArea(){
return PI*radius*radius;
}
// 圆的周长
public double getPerimeter(){
return 2*PI*radius;
}
}
·······································································
// 创建三个圆形
Circle circle1 = new Circle();
Circle circle2 = new Circle();
Circle circle3 = new Circle();
编写一个类Circle,创建三个Circle的实例化对象
。创建是通过new关键字+构造方法()
。接着就可以使用创建出来的对象
来完成属性
的访问
和修饰
,以及调用方法
的操作。
2.2、对象在代码中的体现
在分析现实生活中的事物时发现,这些事物都有其具体的特点和功能,这些特点和功能就组成了这个特殊的事物。
描述小汽车,分析其具有的属性
和行为
。
事物的特点(属性)
:颜色,轮胎个数。事物的行为(方法)
:运行。
可以简单理解:属性
就是数值
,其实就是变量
:行为
就是功能
,其实就是函数
。
//文字模拟:
//小汽车
{
颜色;
轮胎个数;
运行(){}
}
//
class Car
{
String color;// 颜色
int number;// 数量
void run()// 运行方法!!!
{
System.out.println(color+"::"+number);
}
}
通过代码的描述,知道类
的真正意义就是在描述事物。属性
和行为
统称为事物中的成员
。
事物的成员分为两种:成员属性
和成员行为
。
成员属性
:在代码中的体现就是成员变量
。
成员行为
:在代码中的体现就是成员函数(方法)
。
运行测试编写好的Car类。
class CarDemo
{
public static void main(String[] args)
{
//测试:Car类中的run方法。
//1,创建Car的对象。给对象起个名字。
Car c = new Car();//c是类类型的变量。c指向了一个具体的Car类型的对象。
//2,通过已有的对象调用该对象的功能。格式:对象.对象成员;
//3,可以对该对象的属性赋值。
c.color = "red";
c.number = 4;
c.run();
}
}
2.3、类与对象的区别
类
是用于描述现实事物的,它将现实事物进行抽象化,模板化描述。将事物的特点(属性)
和行为
封装在其中。比如小汽车的图纸,图纸就是小汽车的模版。图纸上画着小汽车的各种特点和功能要求。
对象
是现实生活中存在的具体的实例
、个体
。即生活中看到每一个事物,以及我们想象中的事物抽象的概念,都是某一类事物的实例
和个体
。而这些个体都属于某一类事物,即这些个体都是某一类事物中的具体的实例。比如,小汽车就是一类事物,而小汽车又是基于小汽车图纸制造出来的真实个体。因此我们生活中的每一个实物都可以理解为是某一类事物的中的一个个体。创建对象通过对象就可以调用具体的属性
和行为
。
2.4、局部变量和成员变量区别
通过以前学习时变量的定义方式和位置,以及现在定义类中属性的特点。总结下面几点异同:
-
定义的位置不同。
成员变量
定义在类
中。局部变量
定义在方法
中或者语句
里面。 -
在内存中的位置不同
成员变量
存储在堆内存
的对象
中。局部变量
存储在栈内存
的方法
中。 -
生命周期不同
成员变量
随着对象
的出现而出现在堆内存
中,随着对象
的消失而从出现而出现在堆内存
中消失。局部变量
随着方法
的运行而出现在栈内存
中,随着方法
的弹栈
而消失。 -
初始化不同
成员变量
因为在堆内存
中,所以有默认的初始化值。局部变量
没有默认的初始化值,必须手动的给其赋值才可以使用。
2.5、匿名对象(匿名对象作为参数传递)
在使用对象
的时候都会给对象
创建引用,并且通过这个引用来使用创建的对象
。看如下代码:
class Car
{
//描述属性。颜色,轮胎数。
String color;
int number;
//描述行为。
void run()
{
System.out.println(color+":"+number);
}
}
class CarDemo
{
public static void main(String[] args)
{
//创建Car.class类的对象。
Car c = new Car();
//调用run方法
c.run();
//创建Car.class类的对象。
Car c2 = new Car();
//调用run方法
c2.run();
}
}
在main()
方法中,每次创建对象,只为了调用其run()
方法,而这时定义的c引用变量,几乎没有什么作用。而c等价于new Car()
;这时可以使用简化书写成new Car().run()
;
new Car()
;这行代码并没有给Car对象创建引用,即没有给对象起名字,这样的代码就称之为匿名对象
。匿名对象
可以简化上述代码。
class CarDemo
{
public static void main(String[] args)
{
//上述代码的简化形式
new Car().run();
new Car().run();
}
}
为了简化书写,会使用匿名对象
。匿名对象
的使用场景:当对象对方法进行调用时,而且只调用一次时,可以简化成匿名对象
来书写。如: Car c = new Car();c.run(); = = = > new Car().run();
匿名对象
也可以作为参数传递:画图说明匿名对象
传递的细节。
class CarDemo
{
public static void main(String[] args)
{
Car c = new Car();
show(c); //简化成 show(new Car());//把匿名对象作为实际参数进行传递。
}
public static void show(Car cc) // Car cc = c; Car cc = new Car();
{
cc.color = "red";
cc.number = 4;
cc.run();
}
}
2.7、基本类型和引用类型作为参数传递
基本类型
作为参数传递时,其实就是将基本类型变量x空间中的值复制了一份传递给调用的方法show(),先在show()方法中接受x复制的值,再在show()方法中对x变量进行操作,这时只会影响到show()中的x。当==show()方法执行完成弹栈
后,程序又回到main()==方法执行,main()方法中的x值还是原来的值。
当引用变量
作为参数传递时,这时其实是将引用变量空间中的内存地址(引用)复制了一份传递给了show()方法的d引用变量。这时会有两个引用同时指向堆中的同一个对象。当执行show()方法中的d.x=6时,会根据d所持有的引用找到堆中的对象,并将其x属性的值改为6然后==show()==方法弹栈。
由于是两个引用指向同一个对象,不管是哪一个引用改变了引用的所指向的对象的中的值,其他引用再次使用都是改变后的值。
class Demo
{
int x ;
public static void main(String[] args)
{
Demo d = new Demo();
d.x = 5;
show(new Demo());
System.out.println("x="+d.x);
}
public static void show(Demo d)
{
d.x = 6;
}
}
3、封装
3.1、封装概念
之前代码中将**具体功能封装到方法
中,对象
里将方法
封装在类
**中,其实这些都是封装
。
封装
表现:
函数
就是一个最基本封装体。类
其实也是一个封装体。
从以上两点得出结论,封装的好处:
- 提高了代码的复用性。
- 隐藏了实现细节,还要对外提供可以访问的方式。便于调用者的使用,这也是核心之一,也可以理解为就是封装的概念。
- 提高了安全性。
它也是面向对象思想的特征之一。面向对象共有三个特征:封装,继承,多态。
3.2、封装举例
计算机的主机机箱组成部分:
一台电脑,它是由CPU、主板、显卡、内存、硬盘、电源等部件组成,其实我们将这些部件组装在一起就可以使用电脑了,但是发现这些部件都散落在外面,很容造成不安全因素。于是,使用机箱壳子把这些部件都装在里面,并在机箱壳上留下一些插口等,方便外部使用和拓展。
总结:机箱其实就是隐藏了办卡设备的细节,对外提供了插口以及开关等访问内部细节的方式。
3.3、访问修饰符:私有private
观察以下封装实例,分析问题。
/**
* 描述人。Person
* 属性:年龄。
* 行为:说话:说出自己的年龄
*/
public class Person
{
int age ;// 年龄
String name;// 姓名
public void show()// 展示
{
System.out.println("age="+age+",name"+name);
}
}
class PersonDemo
{
public static void main(String[] args)
{
//创建Person对象
Person p = new Person();
p.age = -20; // 给Person对象赋值
p.name = "人妖";
p.show(); //调用Person的show方法
}
}
通过上述代码发现,虽然我们用Java代码把Person描述清楚了,但有个严重的问题,就是Person中的属性
和行为
可以任意访问和使用,这明显不符合实际需求。
可是怎么才能不让访问呢?需要使用一个Java中的关键字也是一个修饰符 private(私有,权限修饰符)
。只要将Person的属性
和行为
私有起来,这样就无法直接访问。
class Person
{
private int age ;
private String name;
public void show()
{
System.out.println("age="+age+",name"+name);
}
}
年龄已被私有,错误的值无法赋值,可是正确的值也赋值不了,这样还是不行,那肿么办呢?按照之前所学习的封装
的原理,隐藏后还需要提供访问方式。只要对外提供可以访问的方法
,让其他程序访问这些方法。同时在方法
中可以对数据进行验证。
一般对成员属性
的访问动作:赋值(设置 set),取值(获取 get),因此对私有的变量访问的方式可以提供对应的 setXxx()
或者getXxx()
的方法。``setXxx()/getXxx`只是推荐写法*,并不是说必须这么写*。
class Person
{
// 私有成员变量
private int age ;
private String name;
// 对外提供设置成员变量的方法
public void setAge(int a)
{
// 由于是设置成员变量的值,这里可以加入数据的验证
if( a < 0 || a > 130 )
{
System.out.println(a+"不符合年龄的数据范围");
return;
}
age = a;
}
// 对外提供访问成员变量的方法
public void getAge()
{
return age;
}
}
总结:
类中不需要对外提供的内容都私有化,包括属性和行为。
以后再描述事物,属性都私有化,并提供setXxx和getXxx方法对其进行访问。
注意:私有仅仅是封装的体现形式而已。
由于Person中的属性
都被private
了,外界无法直接访问属性,必须对外提供相应的setXxx()
和getXxx()
方法。当创建Person对象的时候,Person对象一创建就要明确其姓名和年龄,那该怎么做呢?
3.4、构造函数介绍
在开发中经常需要在创建对象的同时明确对象的属性值,比如员工入职公司就要明确他的姓名、年龄等属性信息。
创建对象就要明确属性值
,可以使用构造函数
。
构造函数
:从字面上理解即为构建创造时用的函数,即就是对象创建时要执行的函数。既然是对象创建时要执行的函数,那么只要在new()
对象时,知道其执行的构造函数是什么,就可以在执行这个函数的时候给对象进行属性赋值。
构造函数的格式:
修饰符 构造函数名(参数列表)
{
}
构造函数
的特点:
构造函数
没有返回值类型。也不需要写返回值。因为==其是为构建对象的,对象创建完,函数就执行结束==。构造函数
名称必须和类名
保持一致。构造函数
没有具体的返回值。
//构造函数的代码体现:
class Person
{
//Person的成员属性age和name
private int age;
private String name;
//Person的构造函数,拥有参数列表
Person(int a , String nm)
{
//接受到创建对象时传递进来的值,将值赋给成员属性
age = a;
name = nm;
}
}
3.5、构造函数调用和内存图解
在描述一个事物的时候,同时也描述了构造函数
,这个构造函数是如何执行的呢?构造函数
是专门用来创建对象的,也就是在new对象
是要调用构造函数
。现在来看看如何调用构造函数。
class Person
{
//Person的成员属性age和name
private int age;
private String name;
//Person的构造函数,拥有参数列表
Person(int a , String nm)
{
//接受到创建对象时传递进来的值,将值赋给成员属性
age = a;
name = nm;
}
public void speak()
{
System.out.println("name="+name+",age="+age);
}
}
class PersonDemo
{
public static void main(String[] args)
{
//创建Person对象
//Person p = new Person();
// 定义了其他的构造方法 空构造就没有了Person()
//上述代码编译报错,原因是在Person类中没有Person()这样的构造函数
//创建Person对象,并明确对象的年龄和姓名
Person p2 = new Person(23,"张三");
p2.speak();
}
}
上述代码演示了创建对象时构造函数的调用。即在创建对象时,会调用与参数列表对应的构造函数。
- 首先会将
main()
函数压入栈中,执行main()
函数中的 new Person(23,“张三”); - 在
堆内存
中分配一片区域,用来存放创建的Person对象,这片内存区域会有属于自己的内存地址(0x88)。然后给成员变量进行默认初始化(name = null,age = 0)。 - 执行
构造函数
(这里省略部分知识,后面讲解)中的代码(age = a ; name = nm;),这段代码执行结束后,成员变量age和name的值已经改变。此步骤也称为构造函数初始化。执行结束之后构造函数弹栈,Person对象创建完成。将Person对象的内存地址0x88赋值给p2。
3.6、默认构造函数和细节
描述事物时,并没有显示指定构造函数,当在编译Java文件时,编译器会自动给class文件中添加默认的构造函数。如果在描述事物时,显示指定了构造函数,当在编译Java源文件时,编译器就不会再给class文件中添加默认构造函数。
class Person
{
//如果没有显示指定构造函数,编译会在编译时自动添加默认的构造函数
//Person(){} //空参数的默认构造函数
}
当在描述事物时,要不要在类中写构造函数呢?这是要根据描述事物的特点来确定,当描述的事物在创建其对象时就明确属性的值,这时就需要在定义类的时候书写带参数的构造函数。若创建对象时不需要明确具体的数据,这时可以不用书写构造函数(不书写也有默认的构造函数)。
构造函数的细节:
- 一个
类
中可以有多个构造函数
,多个构造函数是以重载
的形式存在的。 构造函数
中也是有return
语句的,用于结束初始化动作。构造函数
是可以被private
修饰的,作用:其他程序无法创建该类的对象。
class Person
{
private int age;
private String name;
//私有无参数的构造函数,即外界不能通过new Person();语句创建本类对象
private Person()
{
}
//多个构造函数是以重载的形式存在
Person(int a)
{
age = a;
}
Person(String nm , int a)
{
name = nm;
age = a;
}
}
理解清楚:构造函数
和普通函数
的区别。
3.7、构造函数和一般函数区别
构造函数
在对象创建时就执行了,而且只执行一次。
一般函数
是在对象创建后,需要使用时才被对象调用,并可以被多次调用。
问题:有了构造函数之后可以对对象的属性进行初始化,那么还需要对应的set和get方法吗?
需要相应的setXxx()
和getXxx()
方法,因为对象在创建之后需要修改和访问相应的属性值,这时只能通过setXxx()
或者getXxx()
方法来操作。
3.8、this调用构造函数
在之前学习函数之间调用时,可以通过函数名进行调用。可是针对构造函数,无法通过构造函数名来相互调用。
构造函数
之间的调用可以通过this关键字
来完成。
class Person
{
// Person的成员属性
private int age;
private String name;
//无参数的构造函数
Person()
{
}
//给姓名初始化的构造函数
Person(String nm)
{
name = nm;
}
//给姓名和年龄初始化的构造函数
Person(String nm , int a)
{
//由于已经存在给姓名进行初始化的构造函数 name = nm;因此只需要调用即可
//调用其他构造函数,需要通过this关键字来调用
this(nm);
//给年龄初始化
age = a;
}
}
构造函数
之间的相互调用可以通过this关键字
完成。
构造函数调用格式:
this(参数列表);
方法的自调用
this.XXX(参数列表)
3.9、this的原理图解
图解this关键字和构造函数:
class Person
{
private int age;
private String name;
Person()
{
}
Person(String nm)
{
// this -- 指向当前对象
name = nm;
}
Person(String nm , int a)
{
// this -- 指向当前对象
this(nm);// 等效于Person(nm)
age = a;
}
}
class PersonDemo
{
public static void main(String[] args)
{
Person p = new Person("张三",23);
}
}
// 创建对象时看的是new关键字,有new有对象
// 构造方法只是进一步完成初始化属性的。真正创建对象是 new + 构造方法()
- 先执行
main()
方法,main()
方法压栈,执行其中的new Person(“张三”,23); 堆内存
中开辟空间,并为其分配内存地址0x33,紧接着成员变量默认初始化(name = null , age = 0);- 拥有两个参数的
构造函数
(Person(String nm , int a))压栈,在这个构造函数中有一个隐式的this
,因为构造函数
是给对象初始化的,哪个对象调用到这个构造函数,this
就指向堆中的构造函数创造出来的对象。 - 由于Person(String nm , int a)
构造函数
中使用了this(nm)
;构造函数
Person(String nm)就会压栈,并将“张三”传递给nm。在Person(String nm )构造函数
中同样也有隐式的this,this的值同样也为0x33,这时会执行其中name = nm,即把“张三”赋值给成员的name。当赋值结束后==Person(String nm )==构造函数弹栈。 - 程序继续执行
构造函数
Person(String nm , int a)中的age = a;这时会将23赋值给成员属性age。赋值结束构造函数
Person(String nm , int a)弹栈。 - 当
构造函数
Person(String nm , int a)弹栈结束后,Person对象在内存中创建完成,并将0x33赋值给main()方法
中的p引用变量。
注意:
this到底代表什么呢?this代表的是对象,具体代表哪个对象呢?哪个对象调用了this所在的函数,this就代表哪个对象。
调用其他构造函数的语句必须定义在构造函数的第一行,原因是初始化动作要最先执行。
3.10、成员变量和局部变量同名问题
当在函数中出现了局部变量
和成员变量
同名的时候,那么在方法中怎么区别局部变量成员变量呢?可以在成员变量
名前面加上this来区别成员变量
和局部变量
class Person
{
private int age;
private String name;
//给姓名和年龄初始化的构造函数
Person(String name , int age)
{
//当需要访问成员变量是,只需要在成员变量前面加上this.即可
this.name = name;
this.age = age;
}
public void speak()
{
System.out.println("name="+this.name+",age="+this.age);
}
}
class PersonDemo
{
public static void main(String[] args)
{
Person p = new Person("张三",23);
p.speak();
}
}
3.11、this的应用
class Person
{
private int age;
private String name;
//给姓名和年龄初始化的构造函数
Person(String name , int age)
{
//当需要访问成员变量是,只需要在成员变量前面加上this.即可
this.name = name;
this.age = age;
}
public void speak()
{
System.out.println("name="+this.name+",age="+this.age);
}
//判断是否为同龄人
public boolean equalsAge(Person p)
{
//使用当前调用该equalsAge方法对象的age和传递进来p的age进行比较
//由于无法确定具体是哪一个对象调用equalsAge方法,这里就可以使用this来代替
/*
if(this.age == p.age)
{
return true;
}
return false;
*/
return this.age == p.age;
}
}
this.属性 this(参数列表) this.方法()
4、static关键字
4.1、static关键字-概述
当在定义类的时候,类中都会有相应的属性
和行为
。而属性
和行为
都是通过创建本类对象调用的。当在调用对象的某个行为时,这个行为没有访问到对象的特有数据时,调用方法而创建这个对象有些多余。可是不创建对象,行为又调用不了,这时就会想,那么我们能不能不创建对象,就可以调用行为呢?
class Person
{
private int age;
private String name;
Person(int age,String name)
{
this.age = age;
this.name = name;
}
//说话的行为,说出自己的年龄和姓名
void speak()
{
System.out.println("name="+this.name+",age="+this.age);
}
//睡觉行为
void sleep()
{
System.out.println("睡觉ZZZzzz....");
}
}
class PersonDemo
{
public static void main(String[] args)
{
Person p = new Person(23,"张三");
p.speak();
p.sleep();
}
}
若只为调用sleep()
方法而创建对象,没有任何意义,若当sleep()
方法要多次调用时,就会创建多个Person对象,更加显得多余。
4.2、static关键字-修饰函数
如果创建对象调用方法,发现这个方法中没有使用到对象中的特有数据,那么创建该对象仅仅是为了调用方法,就显得这个对象创建很多余,这时可以使用static关键字
修饰这个方法。
若一个方法被static关键字
修饰,则该方法属于类的方法,可以通过类名的方式直接调用。
class Person
{
private int age;
private String name;
Person(int age ,String name)
{
this.age = age;
this.name = name;
}
//说话的行为,说出自己的年龄和姓名
void speak()
{
sleep();
System.out.println("name="+this.name+",age="+this.age);
}
//睡觉行为,由于sleep方法没有访问对象的特有数据,可以使用静态修饰
static void sleep()
{
System.out.println("睡觉ZZZzzz....");
}
}
class PersonDemo
{
public static void main(String[] args)
{
//sleep方法是静态方法,属于类的方法,可以使用类名直接调用 new Person()
Person.sleep();
}
}
什么时候使用静态修饰方法呢?
定义功能时,如果功能不需要访问类中定义的成员变量(非静态)时,该功能就需要静态修饰。
4.3、静态方法的使用注意事项
通过上面的演示,发现被静态修饰的方法中无法访问非静态的属性
和方法
。
静态
是随着类的加载就加载了。也是随着类的消失而消失了。静态
优先于对象
存在,被所有对象
共享。- 因为
静态
先存在于内存中无法访问后来的对象的中的数据,所以**静态
无法访问非静态**。而且内部无法书写this。因为这时对象有可能不存在,this没有任何指向。
静态
方法使用注意事项:
静态方法
不能访问非静态的成员。但是非静态可以访问静态成员
的。
说明:静态
的弊端在于访问出现局限性。好处是可以直接被类名调用。
静态方法
中不允许出现this,super关键字。
main()
方法其实也是静态的。因为main()
是程序的入口,是提供给JVM使用的,当在dos中输入java XXX 时,会启动JVM,同时**JVM
会加载以XXX为名称的这个class文件进内存**。并扫描其中有没有main()
方法。若有main()
方法JVM就会去调用这个main()
方法。JVM调用main()
方法,是不会创建对象的,没有对象怎么调用方法,这个方法只能被静态修饰
。JVM通过类名调用的。
4.4、静态变量
静态不仅可以修饰方法
,同时静态也可以修饰成员变量
。
class Circle
{
//圆的半径
private double radius = 10;
//圆周率,由于圆周率是固定值,因此所有对象共享这个数据
//没有必要每个对象中拥有这个数据,因此可以使用静态修饰
static double pi = 3.14;
//带参数的构造函数
Circle(double radius)
{
this.radius = radius;
}
//获取圆面积
double getArea()
{
return radius * radius * pi;
}
}
class CircleDemo
{
public static void main(String[] args)
{
System.out.println(new Circle(3).getArea());
}
}
如果pi这个变量没有被静态修饰的话,当创建Circle对象时,每个对象中都会有pi这个变量,但是pi是个固定不变的值,没有必要每个对象中拥有,这时可以将这个变量静态修饰,让所有对象共享就可以了。
4.5、静态变量和成员变量的区别
静态变量
和成员变量
的区别:
- 变量所属不同
静态变量
所属于类
,也称为类变量
。
成员变量
所属于对象
,也称为实例变量(对象变量)
。
- 内存中的位置
静态变量
存储于方法区
中的静态区
中。
成员变量
存储于堆内存
中。
- 在内存中出现的时间
静态变量
随着类的加载而加载,随着类的消失而消失。
成员变量
随着对象的创建而在堆内存中出现,随着对象的消失而消失。
4.6、静态加载的内存图解
public class Person
{
//人的姓名
private String name;
//所有对象都具有国籍属性,并且都会CN,这时可以将这个成员变量定义成静态的
private static String country = "CN";
Person(String name)
{
this.name = name;
}
void showName()
{
System.out.println("name="+name);
}
//静态方法
static void showCountry()
{
System.out.println("Country="+country);
}
}
class StaticDemo
{
public static void main(String[] args)
{
Person p = new Person("123");
p.showName();
//showCountry是静态方法,类名直接调用
Person.showCountry();
}
}
方法区:包含的是整个程序唯一的元素,如class和static,常量池也在方法区。
4.7、静态代码块
静态代码块
,其实就在代码块前面加上了静态关键字
,这个代码块就称为静态代码块
。
优先于主方法执行,优先于构造代码块执行,不管创建多少对象,静态代码块只执行一次,可用于给静态变量赋值; 用来给类进行初始化。
public class Person {
private String name;
private int age;
static{
System.out.println("静态代码块执行了");
}
}
静态代码块的应用场景:类不需要创建对象,但需要初始化,这时可以将部分代码存储到静态代码块中
。
4.8、构造代码块&局部代码块
普通代码块
就是直接定义在方法或语句中定义的代码块:
class Demo{
public static void main(String[] args)
{
{
int x = 1;
System.out.println("普通代码块" + x);
}
int x = 99;
System.out.println("代码块之外" + x);
}
}
//结果:
//普通代码块1
//代码块之外99
直接写在类中的代码块
:优先于构造方法执行,构造代码块用于给所有对象初始化用,每创建一个对象就会执行一次构造代码块。构造函数用于给指定对象初始化。
public class Person {
private String name;
private int age;
static{
System.out.println("静态代码块执行了");
}
{
System.out.println("构造代码块执行了");
}
Person(){
System.out.println("Person无参数的构造函数执行");
}
Person(int age){
this.age = age;
System.out.println("Person(age)参数的构造函数执行");
}
}
class PersonDemo{
public static void main(String[] args)
{
Person p = new Person();
Person p1 = new Person(23);
}
}
代码块里变量的作用域:只在自己所在区域(前后的{})内有效;
局部代码块
:写在局部范围内的代码块。作用
:就可以控制局部变量的生命周期。
class Demo
{
public static void main(String[] args)
{
{//局部代码块
int x = 5;
System.out.println(" 局部代码块..."+x);
}
System.out.println(" over...");
}
}
4.9、对象的创建过程
class Demo
{
static int x = 1; //静态成员变量
int y = 1; //非静态成员变量
static //静态代码块
{
x = 2;
System.out.println("static code...x="+x);
}
{//构造代码块
System.out.println("cons code ...y="+y);
}
Demo() //构造函数
{
System.out.println("cons function ...y="+y);
}
}
class CreateObjectTest
{
public static void main(String[] args)
{
Demo d = new Demo(); //创建Demo对象
}
}
对象加载的流程总结:
- 加载Demo.class文件进
方法区
,并进行空间分配。 - 如果有
静态变量
,先默认初始化,显示初始化值。 - 如果有
静态代码块
,要执行,仅一次。 - 通过
new关键字
在堆内存
中开辟空间,并明确首地址
。 - 将对象中的
非static属性
进行默认初始化。 - 调用对应的
构造函数
进行初始化。 构造函数
内部。
- 调用父类构造函数super(); this() 一个类创建的时候也被动创建了其父类。
成员变量
的显示初始化。构造代码块
初始化。构造函数
内容自定义内容初始化。
- 对象初始化完毕后,将地址赋值给d引用变量。
public class Demo2 {
public static int k = 0;
public static Demo2 t1 = new Demo2("t1");
public static Demo2 t2 = new Demo2("t2");
public static int i = print("i");
public static int j = print("j");
public static int n = 99;
{
print("constructor code");
}
static {
print("static code");
}
public static int print(String s) {
System.out.println("i="+i +" "+s+ " k=" + k + " n=" + n + " j=" + j);
++i;
++k;
++n;
return i;
}
public Demo2(String string) {
print(string);
}
public static void main(String[] args) {
Demo2 d=new Demo2("T");
}
}
4.10、单例模式的场景
设计模式最早来源于建筑领域,是一套被反复使用、多数人知晓的、经过分类、设计经验的总结。使用设计模式是为了可重用、更容易被他人理解、保证可靠性。
设计模式是解决某一种问题的一种思想,是一种行之有效的解决方式。要了解他们各自解决的问题
单例解决的问题:保证一个类的对象在内存中的唯一性,即一个类在内存中的对象有且只有一个。
应用场景:多个程序都在操作同一个配置文件时,需要程序A操作后的结果,程序B要知道,并继续基于程序A操作后的结果进行操作。前提,数据都存储在配置文件对象中,要求程序A和程序B操作的配置文件对象是同一个对象。
要限制其他程序不能随便创建这个类的对象,保证内存只有一个对象。
创建对象是要调用构造函数
的,那么只要将这个类的构造函数
私有化,不让其他程序访问,其他程序就无法创建对象了。但当私有构造函数
后,其他程序无法创建对象。如果需要使用其对象,可以在本类中创建自己的对象,对外提供获取本类对象的方法即可。
4.11、单例模式的代码体现和小问题
饿汉模式
public class Single {
//私有本类中的构造函数
private Single(){}
//创建本类对象
private static Single s = new Single();
//对外提供获取本来对象方法
public static Single getInstance(){
return s;
}
}
注意问题:
由于外界无法创建Single对象,没有对象,那就无法调用getInstance方法,这时需要将getInstance方法静态化,这样外界就可以通过类名直接调用该方法。
懒汉模式
public class Single
{
//私有构造函数
private Single(){}
//在本类中创建本类对象
private static Single instance = null;
//对外提供静态访问方法,获取本类实例对象
public static Single getInstance(){
if(instance == null ) // 这里会有线程安全问题
{
instance = new Single();
}
return instance;
}
}
class SingleDemo
{
public static void main(String[] args)
{
//获取Single类的实例对象s
Single s = Single.getInstance();
//获取Single类的实例对象s2
Single s2 = Single.getInstance();
System.out.println(s==s2); //true
}
}
4.12、单例模式的应用
懒汉模式
,它的特点是**运行时获得对象的速度比较慢,但加载类的时候比较快**。它在整个应用的生命周期只有一部分时间在占用资源。
饿汉模式
,它的特点是**加载类的时候比较慢,但运行时获得对象的速度比较快。它从加载到应用结束会一直占用资源**。
这两种模式对于初始化较快,占用资源少的轻量级对象来说,没有多大的性能差异,选择懒汉式
还是饿汉式
都没有问题。但是对于初始化慢,占用资源多的重量级对象来说,就会有比较明显的差别了。所以,对重量级对象应用饿汉模式
,类加载时速度慢,但运行时速度快;懒汉模式
则与之相反,类加载时速度快,但运行时第一次获得对象的速度慢。
从用户体验的角度来说,我们应该首选饿汉模式
。我们愿意等待某个程序花较长的时间初始化,却不喜欢在程序运行时等待太久,给人一种反应迟钝的感觉,所以对于有重量级对象参与的单例模式,我们推荐使用饿汉模式
。
//描述超人。
class SuperMan
{
private String name;
private static SuperMan man = new SuperMan("克拉克");
private SuperMan(String name)
{
this.name = name;
}
public static SuperMan getInstance()
{
return man;
}
public void fly()
{
System.out.println(this.name +"..fly");
}
}
class SingeTest
{
public static void main(String[] args)
{
//超人对象应该是唯一的。保证superMan的唯一性。可使用单例模式解决。
//SuperMan man = new SuperMan("克拉克");
SuperMan man1 = SuperMan.getInstance();
SuperMan man2 = SuperMan.getInstance();
man1.setName("英雄");
man2.fly();
}
}
5、继承
5.1、引入继承
class Student
{
String name;
int age;
void study()
{
System.out.println("study");
}
}
class Worker
{
String name;
int age;
void work()
{
System.out.println("work");
}
}
通过代码演示描述多个事物。多个事物之间发现有共同的属性
和行为
。那么代码的复用性
很差,那么怎么办呢?
可以将相同的代码进行抽取,抽取出来后放在单独的类中。
class Student
{
void study()
{
System.out.println("study");
}
}
class Worker
{
void work()
{
System.out.println("work");
}
}
class Person
{
String name;
int age;
}
代码抽取到了Person类中,但是Student和Worker类与Person类没有任何关系,那么Student和Worker类如何能使用到Person中的name和age属性。
为了让类与类之间能有关系,需要使用Java中提供的继承
这种机制。继承
需要用到关键字extends
。
//将学生和工人的共性抽取到Person这个类中
class Person {
String name;
int age;
}
//学生通过extends继承Person,即Person为Student的父类
class Student extends Person{
//学生类中的自己特有方法
public void study(){
System.out.println(name+"同学正在学习。。。。");
}
}
//工人通过extends继承Person,即Person为工人的父类
class Worker extends Person{
//工人类中的自己特有方法
public void work(){
System.out.println(name+"工人正在工作。。。。");
}
}
//测试类
public class Test{
public static void main(String[] args) {
Student s = new Student();
s.name = "小明";
s.study();
Worker w = new Worker();
w.name = "张三";
w.work();
}
}
继承的好处:
1、继承
的出现提高了代码的复用性,提高软件开发效率。
2、继承
的出现让类与类之间产生了关系,提供了多态的前提。
5.2、单继承&多继承&多重继承
继承让类与类之间产生了关系,那到底什么时候使用继承呢?
使用继承
,必须保证类与类之间有所属(b is a)关系,即xxx是zzz中的一种。例如:苹果是水果中的一种,狗是犬科中的一种。
Java只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。
class SubDemo extends Demo{} //ok
class SubDemo extends Demo1,Demo2...//error
Java支持多层继承(继承体系)
class A{}
class B extends A{}
class C extends B{}
定义继承需要注意:不要仅为了获取其他类中某个功能而去继承。
日常开发中小技巧:
多层次继承
出现的继承体系中,通常看父类中的功能,了解该体系的基本功能,建立子类对象即可使用该体系功能。
多继承
虽然能使子类同时拥有多个父类的特征,但是其缺点也是很显著的,主要有两方面:
- 如果在一个子类继承的多个父类中拥有相同名字的实例变量,子类在引用该变量时将产生歧义,无法判断应该使用哪个父类的变量。
- 如果在一个子类继承的多个父类中拥有相同方法,子类中有没有覆盖该方法,那么调用该方法时将产生歧义,无法判断应该调用哪个父类的方法。
5.3、继承-子父类中成员变量的特点
成员变量:如果子类父类中出现不同名的成员变量,这时的访问是没有任何问题。
class Fu
{
//Fu中的成员变量。
int num = 5;
}
class Zi extends Fu
{
//Zi中的成员变量
int num2 = 6;
void show()
{
//访问父类中的num
System.out.println("Fu num="+num);
//访问子类中的num2
System.out.println("Zi num2="+num2);
}
}
class Demo
{
public static void main(String[] args)
{
Zi z = new Zi(); //创建子类对象
z.show(); //调用子类中的show方法
}
}
//代码说明:Fu类中的成员变量是非私有的,子类中可以直接访问,若Fu类中的成员变量私有了,子类是不能直接访问的。
当子父类中出现了同名成员变量时,在子类中若要访问父类中的成员变量,必须使用super关键字
来完成。
class Fu
{
//Fu中的成员变量。
int num = 5;
}
class Zi extends Fu
{
//Zi中的成员变量
int num = 6;
void show()
{
//子父类中出现了同名的成员变量时
//在子类中需要访问父类中非私有成员变量时,需要使用super关键字
//访问父类中的num
System.out.println("Fu num="+super.num);
//访问子类中的num2
System.out.println("Zi num2="+this.num);
}
}
class Demo5
{
public static void main(String[] args)
{
Zi z = new Zi(); //创建子类对象
z.show(); //调用子类中的show方法
}
}
流程解析:子类创建的同时,父类记载到内存了。
- 当程序执行
new Zi()
;这时会加载Zi类字节码文件,但由于Zi类继承了Fu类,因此需要将**父类字节码文件**加载进方法区
。 - 当Zi和Fu的字节码加载完毕后。会执行
new Zi()
;即在堆内存
中创建Zi类对象,并为其分配内存地址0x99;对象的内存空间分配结束之后,开始成员变量默认初始化,此时需要注意Fu的num同样也会在此对象中。 - 紧接着开始zi构造函数压栈,在**zi的构造函数中有隐式的super()语句,此时Fu的构造函数也会压栈,Fu的构造函数压栈后会将Fu的num显示初始化**。
- 接着**Fu构造函数弹栈**,执行Zi的构造函数,Zi的num显示初始化。接着Zi构造函数弹栈。此时Zi对象在堆中创建完成,并将内存地址0x99赋值给
main()
方法中的z引用。接着调用zi的show()方法,==show()==方法压栈。
注意:super和this的用法相似,this:代表本类的对象引用。super:代表的父类内存空间,而不是父类的引用。
子父类中同名的成员变量,这种情况开发中不用,因为父类一旦描述完成了属性,子类直接使用就可以了。
5.4、继承-子父类中成员函数特点-重写&应用
当在程序中通过对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。【就近原则
】
class Fu{
public void show(){
System.out.println("Fu类中的show方法执行");
}
}
class Zi extends Fu{
public void show2(){
System.out.println("Zi类中的show2方法执行");
}
}
public class Test{
public static void main(String[] args) {
Zi z = new Zi();
z.show(); //子类中没有show方法,但是可以找到父类方法去执行
z.show2();
}
}
成员方法特殊情况——覆盖
子类中出现与父类一模一样的方法时,会出现覆盖操作,也称为override重写
、复写
或者覆盖
。
class Fu
{
void show()
{
System.out.println("Fu show");
}
}
class Zi extends Fu
{
//子类复写了父类的show方法
void show()
{
System.out.println("Zi show");
}
}
覆盖的应用:
当子类需要父类的功能,而功能主体子类有自己特有内容时,可以复写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容。
举例:比如手机,当描述一个手机时,它具有发短信,打电话,显示来电号码功能,后期由于手机需要在来电显示功能中增加显示姓名和头像,这时可以重新定义一个类描述手机,并继续原有的描述手机的类。并在新定义的类中覆盖来电显示功能,在其中增加显示姓名和头像功能。
public class Test {
public static void main(String[] args) {
new NewPhone().showNum();
}
}
class Phone{
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
class NewPhone extends Phone{
//覆盖父类的来电显示号码功能,并增加自己的显示姓名和图片功能
public void showNum(){
//调用父类已经存在的功能使用super
super.showNum();
//增加自己特有显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
}
5.5、重写的注意事项
重写需要注意的细节问题:
1、子类方法覆盖父类方法,必须要保证**权限大于等于父类权限**。
class Fu()
{ void show(){}
public void method(){}
}
class Zi() extends Fu
{ public void show(){} //编译运行没问题
void method(){} //编译错误
}
// public >缺省> protected > private
2、静态
只能覆盖静态
,或者被静态
覆盖。
3、写法上稍微注意:必须一模一样:函数的返回值类型 函数名 参数列表都要一样。
总结:当一个类是另一个类中的一种时(A is B),可以通过继承
,来扩展功能。如果父类具备的功能内容需要子类特殊定义时,使用重写
。
5.6、子父类中构造函数特点(super())
在创建子类对象时,父类的构造函数
会先执行,因为子类中所有构造函数
的第一行有默认的隐式super()语句,调用本类中的构造函数
用this(实参列表)语句,调用父类中的构造函数
用super(实参列表)。
为什么**子类对象初始化都要访问父类中的构造函数
?因为子类继承
了父类的内容**,所以创建对象时必须要先看父类是如何对内容进行初始化的。
public class Test {
public static void main(String[] args) {
new Zi();
}
}
class Fu{
int num ;
Fu(){
System.out.println("Fu构造函数"+num);
num = 4;
}
}
class Zi extends Fu{
Zi(){
System.out.println("Zi构造函数"+num);
}
}
// 执行结果:
// Fu构造函数0
// Zi构造函数4
子类中的构造函数为什么有一句隐式的super()
呢?
原因:子类会继承
父类中的内容,所以**子类在初始化时**,必须先到父类中去执行父类的初始化动作
。才可以更方便的使用父类中的内容。
当**父类中没有空参数构造函数
**时,子类的构造函数必须有显示的super()语句指定要访问的父类中的构造函数
。
class Fu extends Object
{
Fu()
{
//super();
//显示初始化。
System.out.println("fu constructor run..A..");
}
Fu(int x)
{
//显示初始化。
System.out.println("fu constructor run..B.."+x);
}
}
class Zi extends Fu
{
Zi()
{
//super();
System.out.println("zi constructor run..C..");
}
Zi(int x)
{
super();
System.out.println("zi constructor run..D.."+x);
}
}
class Demo
{
public static void main(String[] args)
{
new Zi();
new Zi(3);
}
}
5.7、子类实例化过程的细节
如果子类的构造函数
第一行写了this
调用了本类其他构造函数
,这时是没有super调用父类的语句的,因为this()
或者super()
,只能定义在构造函数
的第一行,因为初始化动作要先执行。
父类构造函数中也有隐式的super()
。记住:只要是构造函数默认第一行都是super()
; Java体系在设计,定义了一个所有对象的父类Object
。
注意:
类中的
构造函数
默认第一行都有**隐式的super()语句,在访问父类中的构造函数
。所以父类的构造函数
既可以给自己的对象初始化**,也可以给自己的子类对象初始化。如果默认的隐式super()语句没有对应的构造函数
,必须在**构造函数中通过this或者super的形式明确调用的构造函数
。**
public class Demo06 {
public static void main(String[] args) {
Zi3 zi3 = new Zi3(7);
zi3.show();
}
}
class Fu3 {
public int num = 3;
public Fu3(int num) {
System.out.println("通过Fu3(int num)完成初始化.");
this.num = num;
}
}
class Zi3 extends Fu3{
public Zi3(){
this(5);
}
public Zi3(int num){
// super()
super(num);
}
public void show(){
System.out.println(num);
}
}
5.8、super()应用
练习:描述学生和工人这两个类,将他们的共性name和age抽取出来存放在父类中,并提供相应的get
和set
方法,同时需要在创建学生和工人对象就必须明确姓名和年龄。
class Person{
private int age;
private String name;
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;
}
}
class Student extends Person {
// Student类的构造函数
Student(String name, int age) {
// 使用super关键字调用父类构造函数,进行相应的初始化动作
super(name, age);
}
public void study() {// Studnet中特有的方法
System.out.println(this.getName() + "同学在学习");
}
}
class Worker extends Person {
Worker(String name, int age) {
// 使用super关键字调用父类构造函数,进行相应的初始化动作
super(name, age);
}
public void work() {// Worker 中特有的方法
System.out.println(this.getName() + "工人在工作");
}
}
public class Test {
public static void main(String[] args) {
new Student("小明",23).study();
new Worker("小李",45).work();
}
}
5.9、final关键字
继承
的出现提高了代码的复用性,并方便开发。但随之也有问题,有些类在描述完之后,不想被继承,或者有些类在中的部分方法功能是固定的,子类不能重写。可是当子类继承了这些特殊类之后,就可以对其中的方法进行重写
。
要解决上述的这些问题,需要使用到一个关键字final
,final
的意思为最终,不可变。final
是个修饰符,它可以修饰类,类的成员,以及局部变量。
final修饰类不可以被继承,但是可以继承其他类。
final修饰的方法不可以被覆盖,但父类中没有被final修饰方法,子类覆盖后可以加final。
final修饰的变量称为常量,这些变量只能赋值一次,定义的时候必须有初始值,不能变。
final修饰的引用类型变量,表示该引用变量的引用不能变,而不是引用所指的对象中的数据还是可以变化的。
当程序中一个**数据是固定不变的,这时为了增加阅读性,可以给该数据起个名字。为了保证这个变量的值不被修改,加上final
修饰**,变量就成为了阅读性很强的常量。书写规范,被final
修饰的常量名所有的**字母都是大写的。如果由多个单词组成单词间通过 _
连接。
通常规范中:常量名称所有字母大写,若有多个单词组成,单词间使用下划线连接。
public static final 修饰的常量称为全局常量;
public static final double PI = 3.14159265358979323846;
public static final String APP_SEPARATOR = "/";
5.10、抽象类-产生&特点&细节
当编写一个类时,我们往往会为该类定义一些方法,这些方法是用来描述该类的行为方式,那么这些方法都有具体的方法体。
方法体:方法后{}及其代码,表示方法具体的执行操作。
但是有的时候,某个父类只是知道子类应该包含怎么样的方法,但是无法准确知道子类如何实现这些方法。比如一个图形类应该有一个求周长的方法,但是不同的图形求周长的算法不一样。
分析事物时,发现了共性内容,就出现向上抽取。会有这样一种特殊情况,就是**功能声明相同,功能主体不同。那么这时也可以抽取**,但只抽取方法声明,不抽取方法主体。那么此方法就是一个抽象方法。
描述狗:行为:吼叫。
描述狼:行为:吼叫。
狗和狼之间有共性,可以进行**向上抽取。抽取它们的所属共性类型**:犬科
。由于狗和狼都具有吼叫功能,但是他们具体怎么吼叫却不一样。这时在描述犬科时,发现了有些==功能(吼叫)==不具体,这些不具体的功能,需要在类中标识出来,通过java中的关键字abstract(抽象)
。
当定义了抽象函数
的类
也必须被abstract关键字修饰,被abstract关键字修饰的类是抽象类
。
abstract class 犬科
{
abstract void 吼叫();//抽象函数。需要abstract修饰,并分号;结束
}
class Dog extends 犬科
{
void 吼叫()
{
System.out.println("汪汪汪汪");
}
}
class Wolf extends 犬科
{
void 吼叫()
{
System.out.println("嗷嗷嗷嗷");
}
}
抽象类的特点:
1、抽象类
和抽象方法
都需要被abstract修饰。抽象方法
一定要**定义在抽象类
**中。
2、抽象类
不可以创建实例,原因:调用抽象方法没有意义。
3、只有覆盖了抽象类
中所有的抽象方法
后,其子类才可以实例化。否则该子类还是一个抽象类
。
之所以继承,更多的是在思想,是面对共性类型操作会更简单。
细节问题:
1、抽象类
一定是个父类
?
是的,因为不断抽取而来的。
2、抽象类
是否有构造函数
?
有,虽然不能给自己的对象初始化,但是可以给自己的子类对象初始化。
抽象类
和一般类
的异同点:
相同:
1、它们都是用来描述事物的。
2、它们之中都可以定义属性和行为。
不同:
1、一般**类
可以具体的描述事物。抽象类
描述事物的信息不具体。**
2、抽象类
中可以多定义一个成员:抽象函数
。
3、一般类
可以创建对象,而抽象类
一定不能创建对象。
3、抽象类
中是否可以不定义抽象方法。
是可以的,那这个抽象类的存在到底有什么意义呢?仅仅是不让该类创建对象。
4、抽象关键字abstract不可以和哪些关键字共存?
1、final:fianl修饰的类
是无法被继承的,而abstract修饰的类
一定要有子类。 final修饰的方法
无法被覆盖,但是abstract修饰的方法
必须要被子类去实现的。
2、static:static修饰的方法属于类的,它存在与静态区中,和对象就没关系了。而抽象方法没有方法体,使用类名调用它没有任何意义。
3、private:private的方法子类是无法继承的,也不存在覆盖,而abstract和private一起使用修饰方法,abstract既要子类去实现这个方法,而private修饰子类根本无法得到父类这个方法,互相矛盾。
6、抽象类&&接口
需求:公司中程序员有姓名,工号,薪水,工作内容。
项目经理除了有姓名,工号,薪水,还有奖金,工作内容。对给出需求进行数据建模。
程序员:
属性:姓名,工号,薪水
行为:工作
项目经理:
属性:姓名,工号,薪水,奖金
行为:工作
分析发现程序员和项目经理之间的共性,应该存在着关系。可以将他们的共性向上抽取到共性类型:员工。
员工:
属性:姓名,工号,薪水
行为:工作
发现员工的工作内容本身就不具体。应该是抽象的,由具体的子类来体现的。
abstract class Employee
{
private String name;
private String id;
private double pay;
//构造一个员工对象,一初始化就具备着三个属性。
public Employee(String name,String id,double pay)
{
this.name = name;
this.id = id;
this.pay = pay;
}
//工作行为,由于工作内容不具体,用抽象类描述
public abstract void work();
}
//具体的子类:程序员。
class Programmer extends Employee
{
public Programmer(String name,String id,double pay)
{
super(name,id,pay);
}
public void work()
{
System.out.println("code....");
}
}
//具体的子类:经理。
class Manager extends Employee
{
//特有属性。
private double bonus;
public Manager(String name,String id,double pay,double bonus)
{
super(name,id,pay);
this.bonus = bonus;
}
public void work()
{
System.out.println("manage");
}
}
6.1、接口-定义&实现
当一个抽象类
中的所有方法都是抽象方法
时,那么这个**抽象类
就可以使用接口
这种机制来体现**。
接口怎么定义呢?定义普通的类或者抽象类
可以使用class
关键字,定义接口必须interface关键字完成。
1.8之前,可以说接口就是极度的抽象类,里面全是抽象方法。而1.8之后抽象类里也会有非抽象方法,我们区分两者多以设计的角度,如果需要使用到具体的属性和方法我们使用抽象类
,其他情况我们用接口
。
interface Demo
{
abstract void show1();
abstract void show2();
}
//接口中只能定义常量
接口中成员的特点:
1、接口
中可以定义变量,但是变量必须有固定的修饰符修饰,public static final 所以接口中的变量也称之为**常量
**。
2、接口
中可以定义方法,方法也有固定的修饰符,public abstract。
3、接口
中的成员都是公共的。
4、接口
不可以创建对象。
5、子类必须覆盖掉接口
中所有的抽象方法后,子类才可以实例化
。否则子类是一个抽象类。
接口(1.8之前全部是抽象方法)–抽象类(部分抽象方法)–类(没有抽象方法)
interface Demo//定义一个名称为Demo的接口。
{
public static final int NUM = 3;
public abstract void show1();
public abstract void show2();
}
//定义子类去覆盖接口中的方法。子类必须和接口产生关系,类与类的关系是继承,类与接口之间的关系是 实现。通过 关键字 implements
class DemoImpl implements Demo//子类实现Demo接口。
{
//重写接口中的方法。
public void show1(){}
public void show2(){}
}
6.2、接口-多实现
**接口最重要的体现:**解决多继承的弊端。将多继承这种机制在java中通过多实现完成了。
interface A
{
void abstract show1();
}
interface B
{
void abstract show2();
}
class C implements A,B// 多实现。同时实现多个接口。
{
public void show1(){}
public void show2(){}
}
解决多继承的弊端
弊端:多继承时,当多个父类中有相同功能时,子类调用会产生不确定性。
其实核心原因就是在于多继承父类中功能有主体,而导致调用运行时,不确定运行哪个主体内容。
而在多实现里因为接口中的功能都没有方法体,由子类来明确,来源确定就是实现他们的实现类。
6.3、子类继承类同时实现接口
子类通过继承父类扩展功能,通过继承扩展的功能都是子类应该具备的基础功能。如果子类想要继续扩展其他类中的功能呢?这时通过实现接口来完成。
class Fu
{
public void show(){}
}
interface Inter
{
public abstract void show1();
}
class Zi extends Fu implements Inter
{
public void show1()
{}
}
6.4、接口的多继承
多个接口之间可以使用extends
进行继承
。
interface A{
abstract void show();
}
interface B{
abstract void show1();
}
interface C{
abstract void show2();
}
interface D extends A,B,C{
abstract void show3();
}
在开发中如果多个接口中存在相同方法,这时若有个类实现了这些接口,那么就要实现所有接口中的方法,由于接口中的方法是抽象方法,子类实现后也不会发生调用的不确定性。
6.5、没有抽象方法的抽象类的由来
在开发中,若一个接口
中有多个抽象方法,但是实现这个接口只使用其中某些方法时,这时我们仍然需要将其他不使用的方法实现,这样明显不符合我们的要求,但不实现这些方法又不行。那么怎么办呢?可以使用一个抽象类
,作为过渡,而这个抽象类
实现这个接口
,所有方法都以空实现存在。这就是没有抽象方法的抽象类的存在价值。我们只要继承这个抽象类,覆盖其中需要使用的方法即可。
//拥有多个方法的接口
interface Inter{
abstract void show();
abstract void show1();
abstract void show2();
abstract void show3();
}
//作为过度的抽象类,此类将接口的所有方法都实现,这里的实现是空实现
abstract class AbsInter implements Inter{
public void show(){}
public void show1(){}
public void show2(){}
public void show3(){}
}
/*
//此类直接实现Inter,但只使用其他show和show2方法,这样导致其他两个方法也要实现
//不符合我们的要求
class SubInter2 implements Inter{
public void show() {
System.out.println("show");
}
public void show1() {
}
public void show2() {
System.out.println("show2");
}
public void show3() {
}
}
*/
//此类只使用接口中的show和show2方法,只要覆盖抽象类中的show和show2即可
class SubInter extends AbsInter{
public void show(){
System.out.println("show");
}
public void show2(){
System.out.println("show2")
}
}
7、接口
7.1、接口的思想
前面学习了接口的代码体现,现在来学习接口的思想,接下来从生活中的例子进行说明。
举例:我们都知道电脑上留有很多个插口,而这些插口可以插入相应的设备,这些设备为什么能插在上面呢?主要原因是这些设备在生产的时候符合了这个插口的使用规则,否则将无法插入接口中,更无法使用。发现这个插口的出现让我们使用更多的设备。
总结接口在开发中的它好处:
1、接口
的出现扩展了功能。
2、接口
其实就是暴露出来的规则。
3、接口
的出现降低了耦合性,即设备与设备之间实现了解耦。
接口
的出现方便后期使用和维护,一方是在使用接口(如电脑),一方在实现接口(插在插口上的设备)。例如:笔记本使用这个规则(接口),电脑外围设备实现这个规则(接口)。
7.2、接口和抽象的区别
通过实例进行分析和代码演示抽象类和接口的用法。
抽象类
像是对现有事物的归纳和总结,其属性和行为是在后续子类得以发扬和使用。接口像一种抽离,后续实现是对它的补充说明,它仅仅定义规则。一个先天一个后天
7.2.1、举例:
犬:
行为:
吼叫;
吃饭;
缉毒犬:
行为:
吼叫;
吃饭;
缉毒;
7.2.2、思考:
由于犬分为很多种类,他们吼叫和吃饭的方式不一样,在描述的时候不能具体化,也就是吼叫和吃饭的行为不能明确描述行为时,行为的具体动作不能明确,可以将这个行为写为抽象行为,那么这个类也就是抽象类
。
可是当犬有其他额外功能时,而这个功能并不在这个事物的体系中。这时可以让犬具备犬科自身特点的同时,也有其他额外功能,可以将这个额外功能定义接口
中。
interface 缉毒{
abstract void 缉毒();
}
//定义犬科的这个提醒的共性功能
abstract class 犬科{
abstract void 吃饭();
abstract void 吼叫();
}
// 缉毒犬属于犬科一种,让其继承犬科,获取的犬科的特性,
//由于缉毒犬具有缉毒功能,那么它只要实现缉毒接口即可,这样即保证缉毒犬具备犬科的特性,也拥有了缉毒的功能
class 缉毒犬 extends 犬科 implements 缉毒{
public void 缉毒() {
}
void 吃饭() {
}
void 吼叫() {
}
}
class 缉毒猪 implements 缉毒{
public void 缉毒() {
}
}
7.2.3、通过上面的例子总结接口和抽象类的区别
相同点:
-
都位于继承的顶端,用于被其他
实现
或继承
; -
都不能实例化;
-
都包含
抽象方法
,其子类都必须覆写这些抽象方法;
区别:
-
抽象类
为部分方法提供实现,避免子类重复实现这些方法,提供代码重用性;接口只能包含抽象方法
,极度的抽象类
; -
一个类只能
继承
一个直接父类(可能是抽象类),却可以实现多个接口;接口弥补了Java的单继承的不足。
二者的选用:
- 优先选用
接口
,尽量少用抽象类
; - 需要定义子类的行为,又要为子类提供共性功能时才选用
抽象类
;
8、多态
8.1、多态-好处&弊端&前提
首先通过代码引出多态。
//描述狗,狗有吃饭,看家的行为
class Dog
{
public void eat()
{
System.out.println("啃骨头");
}
public void lookHome()
{
System.out.println("看家");
}
}
//描述猫,猫有吃饭,抓老鼠行为
class Cat
{
public void eat()
{
System.out.println("吃鱼");
}
public void catchMouse()
{
System.out.println("抓老鼠");
}
}
class DuoTaiDemo
{
public static void main(String[] args)
{
/*有多个狗和猫对象都要调用吃饭这个行为
这样会导致d.eat();代码重复性非常严重
Dog d = new Dog();
d.eat();
为了提高代码的复用性,可以将d.eat();代码进行封装
public static void method(Dog d)
{
d.eat();
}
然后创建对象,直接调用method方法即可
method(new Dog());
method(new Dog());
但当创建Cat对象时,同样需要调用eat方法,同样可以将eat方法进行封装
public static void method(Cat c)
{
c.eat();
}
*/
}
}
后期当有了猪对象时,那么同样要封装==eat()==方法,当每多一个动物,都要单独定义功能,**封装方法让动物的对象去做事,会发现代码的扩展性很差。**如何提高代码的扩展性呢?发现既然是让动物去eat,无论是dog,还是cat,eat是他们的共性,那么将eat进行抽取,抽取到父类中。
abstract class Animal
{
//由于每一个小动物的eat方式都不一样,因此在父类中无法准确描述eat的具体行为
//因此只能使用抽象方法描述,从而导致这个类也为抽象类
abstract public void eat();
}
当有了Animal抽象类之后,狗和猫只要继承这个类,实现他们特有的eat方法即可。
//描述狗,狗有吃饭,看家的行为
class Dog extends Animal
{
public void eat()
{
System.out.println("啃骨头");
}
public void lookHome()
{
System.out.println("看家");
}
}
//描述猫,猫有吃饭,抓老鼠行为
class Cat extends Animal
{
public void eat()
{
System.out.println("吃鱼");
}
public void catchMouse()
{
System.out.println("抓老鼠");
}
}
既然Dog
属于Animal中一种,Cat
也属于Animal中一种,那么不用具体面对具体的动物,而只要面对Animal即可。
Dog d = new Dog();
Animal a = new Dog();
Cat c = new Cat();
Animal aa = new Cat();
通过上述代码发现,Animal类型既可以接受Dog
类型,也可以接受Cat
类型,当再让动物去做事时,不用面对具体的动物,而只要面对Animal即可。因此上述method方法可以修改为:
// 多态
public static void method(Animal a)
{
a.eat();
}
method(Animal a)
可以接受Animal的子类型的所有小动物,而method方法不用在关心是具体的哪一个类型。即就是只建立Animal的引用就可以接收所有的Dog和Cat对象进来,让它们去eat。从而提高了程序的扩展性。
多态的体现:
父类的引用或者接口的引用指向了自己的子类对象。
Dog d = new Dog();//Dog对象的类型是Dog类型。
Animal a = new Dog();//Dog对象的类型右边是Dog类型,左边Animal类型。
多态的好处:
提高了程序的扩展性。
多态的弊端:
通过父类引用操作子类对象时,只能使用父类中已有的方法,不能操作子类特有的方法。
多态的前提:
- 必须有关系:
继承,实现
。 - 通常都有
重写
操作。
8.2、多态-转型
当父类的引用 指向子类对象时,就发生了向上转型
,即把子类类型对象转成了父类类型。向上转型的好处是隐藏
了子类类型,提高了代码的扩展性
。
但向上转型也有弊端
,只能使用父类共性的内容,而无法使用子类特有功能,功能有限制。
//描述动物这种事物的共性eat
abstract class Animal{
abstract void eat();
}
//描述dog这类事物
class Dog extends Animal{
void eat(){
System.out.println("啃骨头");
}
void lookHome(){
System.out.println("看家");
}
}
//描述小猫
class Cat extends Animal{
void eat(){
System.out.println("吃鱼");
}
void catchMouse(){
System.out.println("抓老鼠");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); //这里形成了多态
a.eat();
//a.lookHome();//使用Dog特有的方法,需要向下转型
Dog d = (Dog)a;
d.lookHome();
Animal a1 = new Cat();
a1.eat();
/**
* 由于a1具体指向的是Cat的实例,而不是Dog实例,这时将a1强制转成Dog类型,
* 将会发生 ClassCastException 异常,在转之前需要做健壮性判断。
if(!Dog instanceof a1){ // 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;// 方法执行中止
}
Dog d1 = (Dog)a1;
d1.catchMouse();
*/
}
}
向上转型:
不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作,这时就可以使用向上转型
。
向下转型:
当要使用子类特有功能时,就需要使用向下转型
好处:可以使用子类特有功能。
弊端:需要面对具体的子类对象;在向下转型时容易发生ClassCastException类型转换异常。在转换之前必须做类型判断。
8.3、多态-举例
/*
描述毕老师和毕姥爷,
毕老师拥有讲课和看电影功能
毕姥爷拥有讲课和钓鱼功能
*/
class 毕姥爷{
void 讲课(){
System.out.println("政治");
}
void 钓鱼(){
System.out.println("钓鱼");
}
}
//毕老师继承了毕姥爷,就有拥有了毕姥爷的讲课和钓鱼的功能,
//但毕老师和毕姥爷的讲课内容不一样,因此毕老师要覆盖毕姥爷的讲课功能
class 毕老师 extends 毕姥爷{
void 讲课(){
System.out.println("Java");
}
void 看电影(){
System.out.println("看电影");
}
}
// 测试
public class Test {
public static void main(String[] args) {
//多态形式
毕姥爷 a = new 毕老师(); //向上转型
a.讲课(); // 这里表象是毕姥爷,其实真正讲课的仍然是毕老师,因此调用的也是毕老师的讲课功能
a.钓鱼(); // 这里表象是毕姥爷,但对象其实是毕老师,而毕老师继承了毕姥爷,即毕老师也具体钓鱼功能
// 当要调用毕老师特有的看电影功能时,就必须进行类型转换
毕老师 b = (毕老师)a; //向下转型
b.看电影();
}
}
当需使用子类的特有方法时,需要使用到向下转型 转化类型 名称 = (转化类型)对象
,类似与强制类型转化。
总结:转型过程中,至始至终只有毕老师对象做着类型转换,父类对象是无法转成子类对象的。
public class Demo02 {
public static void main(String[] args) {
老夫子 old = new 小先生("萧何",23);
old.传道();
old.授业();
小先生 little = (小先生)old;
little.解惑();
}
}
class 老夫子{
private String name;
private int age;
public 老夫子(String name,int age){
this.name = name;
this.age = age;
}
public void 传道(){
System.out.println("修身齐家治国平天下!!!");
}
public void 授业(){
System.out.println("礼、乐、射、御、书、数");
}
}
class 小先生 extends 老夫子{
public 小先生(String name,int age){
super(name, age);
}
public void 授业(){
System.out.println("以前祖上教六艺,现在我讲Java...");
}
public void 解惑() {
System.out.println("不忘初心!!!");
}
}
多态-练习-笔记本电脑
需求:
- 阶段一:使用笔记本,笔记本有运行功能,需要笔记本对象来运行这个功能。
- 阶段二:想使用一个鼠标,又有一个功能使用鼠标,并多了一个鼠标对象。
- 阶段三:还想使用一个键盘,又要多一个使用键盘功能多一个对象。
- 问题:每多一个功能就需要在笔记本对象中定义一个函数,程序扩展性极差。
/**
* 描述笔记本,笔记本使用USB鼠标,USB键盘
* 定义USB接口,笔记本要使用USB设备,即笔记本在生产时需要预留可以插入USB设备的USB接口,即就是笔记本具备使
* 用USB设备的功能,但具体是什么USB设备,笔记本并不关心,只要符合USB规格的设备都可以。
* 鼠标和键盘要能在电脑上使用,那么鼠标和键盘也必须遵守USB规范,不然鼠标和键盘的生产出来无法使用。
*/
//定义鼠标、键盘,笔记本三者之间应该遵守的规则
interface USB{
void open();//开启功能
void close();//关闭功能
}
//鼠标实现USB规则
class Mouse implements USB{
public void open(){
System.out.println("鼠标开启");
}
public void close(){
System.out.println("鼠标关闭");
}
}
//键盘实现USB规则
class KeyBoard implements USB{
public void open(){
System.out.println("键盘开启");
}
public void close(){
System.out.println("键盘关闭");
}
}
// 定义笔记本
class NoteBook{
//笔记本开启运行功能
public void run(){
System.out.println("笔记本运行");
}
//笔记本使用usb设备,这时当笔记本实体调用这个功能时,必须给其传递一个符合USB规则的USB设备
public void useUSB(USB usb){
//判断是否有USB设备
if(usb != null){
usb.open();
usb.close();
}
}
public void shutDown(){
System.out.println("笔记本关闭");
}
}
public class Test {
public static void main(String[] args) {
//创建笔记本实体对象
NoteBook nb = new NoteBook();
//创建鼠标实体对象
Mouse m = new Mouse();
//创建键盘实体对象
KeyBoard kb = new KeyBoard();
//笔记本开启
nb.run();
//笔记本使用鼠标
nb.useUSB(m);
//笔记本使用键盘
nb.useUSB(kb);
//笔记本关闭
nb.shutDown();
}
}
8.4、多态-成员的特点
多态
出现后会导致子父类中的成员变量
有微弱的变化。如下代码所示:
class Fu
{
int num = 4;
}
class Zi extends Fu
{
int num = 5;
}
public class Demo
{
public static void main(String[] args)
{
Fu f = new Zi();
System.out.println(f.num);
Zi z = new Zi();
System.out.println(z.num);
}
}
多态成员变量:
子父类中出现同名的成员变量
时,多态调用该变量时:
编译时期
:参考的是所属的类的父类中是否有被调用的成员变量
。没有,编译失败。
运行时期
:参考的是所属的类的父类中的成员变量
的值。
简单记
:编译
和运行
都参考等号的左边。编译运行
看左边。
class Fu2
{
int num = 4;
void show()
{
System.out.println("Fu2 show num");
}
}
class Zi2 extends Fu2
{
int num = 5;
void show()
{
System.out.println("Zi2 show num");
}
}
class Demo2
{
public static void main(String[] args)
{
Fu f = new Zi();
f.show();
}
}
多态成员函数:
编译时期
:参考子类对象
的父类中没有该调用的函数,没有,编译失败。
运行时期
:参考子类有没有重写该方法,并运行子类对象
的成员函数
。
简而言之
:编译
看左边,运行看右边。
class Fu3
{
int num = 4;
static void method()
{
System.out.println("fu3 static method run");
}
}
class Zi3 extends Fu3
{
int num = 5;
static void method()
{
System.out.println("zi3 static method run");
}
}
class Demo3
{
public static void main(String[] args)
{
Fu3 f = new Zi3();
f.method();
}
}
多态静态函数:
多态调用:编译
和运行
都参考引用类型变量所属的类中的静态函数
。
简而言之:编译
和运行
看等号的左边。其实真正调用静态方法是不需要对象的,静态方法
通类直接调用。
结论:
- 对于
成员变量
和静态函数
,编译
和运行
都看左边。 - 对于
成员函数
,编译
看左边,运行
看右边。
9、Object-概述
Object
类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object
。
JAVA的API中Object类中描述所有对象共有的方法。
在JDK中有src.zip
文件,这个文件夹是所有Java类的源文件。可以在其中查看相对应的类的源码。
9.1、Object类中常用的介绍
9.1.1、equals方法
equals方法,用于比较两个对象是否相同,它其实就是使用对象的内存地址在比较。Object类中的equals方法内部使用的就是==比较运算符。
在开发中要比较两个对象是否相同,经常会根据对象中的特有数据进行比较,也就是在开发经常需要复写equals方法根据对象的特有数据进行比较。
/*
* 描述人这个类,并定义功能根据年龄判断是否是同龄人
* 由于要根据指定类的特有数据进行比较,这时只要覆盖Object中的equals方法
* 在方法体中根据类的特有数据进行比较
*/
class Person extends Object{
int age ;
//复写父类的equals方法,实现自己的比较方式
public boolean equals(Object obj) {
//判断当前调用equals方法的对象和传递进来的对象是否是同一个
if(this == obj){
return true;
}
//判断传递进来的对象是否是Person类型
if(!(obj instanceof Person)){
return false;
}
//将obj向下转型为Perosn引用,调用其特有数据
Person p = (Person)obj;
return this.age == p.age;
}
}
注意:在复写Object中的equals方法时,一定要注意==public boolean equals(Object obj)==的参数是Object类型,在调用对象特有数据时,一定要进行类型转换,在转换之前必须进行类型判断。
9.1.2、toString方法
==toString()==方法返回该对象的字符串表示,其实就是对象的类型+@+哈希值
由于**toString()方法返回的结果是内存地址**,在开发中经常需要按照对象的特定数据得到相应的表现形式,因此也需要重写toString方法。
class Person extends Object{
int age ;
//根据Person类的特有数据复写toString方法
public String toString() {
return "Person [age=" + age + "]";
}
}