你之所以能优于别人,正是因为你坚持了别人所不能坚持的。
本文相关代码在我的Github,欢迎Star~
文章目录
前言
在此之前,我们已经把Java基础的基础语法总结了一下,今天我们来学习一下面向对象的相关知识,今天的内容理论性偏多,希望大家能耐心的看完,相信会收获很多。都说 Java
是面向对象程序设计的语言,那么究竟什么是面向对象呢?如果你没有面向对象程序设计的应用背景,那么和我一起来认真的阅读本文吧!
面向对象程序设计概述
面向对象程序设计(简称 OOP
),是当今主流的设计范型。面向对象程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。在 OOP
中,不必关心对象的具体实现,只要能满足用户的需求即可。
类
类( class)是构造对象的模板或蓝图。由类构造(construct) 对象的过程称为创建类的实例 (instance )。
封装( encapsulation , 有时称为数据隐藏) 是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域( instance field ), 操纵数据的过程称为方法( method ) 。对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态( state )。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。
OOP
的另一个原则会让用户自定义 Java
类变得轻而易举,这就是:可以通过扩展一个类来建立另外一个新的类。在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中,只需提供适用于这个新类的新方法和数据域就可以了。
对象
要想使用 OOP
,一定要清楚对象的三个主要特性
- 对象的行为(behavior)- 可以对对象施加哪些操作,或可以对对象施加哪些方法?
- 对象的状态(state)- 当施加那些方法时,对象如何响应?
- 对象标识(identity)- 如何辨别具有相同行为与状态的不同对象?
识别类
在面向对象的学习中,我们首先要学会设计类,然后在往类中添加所需的方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
例如:在订单处理系统中,有这样一些名词:
- 商品(Item)
- 订单(Order)
- 送货地址(Shipping address)
- 付款(Payment)
- 账户(Account)
这些名词很可能成为类 Item、 Order 等。
接下来, 查看动词:商品被添加到订单中, 订单被发送或取消, 订单货款被支付。对于每一个动词如:“ 添加”、“ 发送”、“ 取消” 以及“ 支付”, 都要标识出主要负责完成相应动作的对象。例如,当一个新的商品添加到订单中时, 那个订单对象就是被指定的对象, 因为它知道如何存储商品以及如何对商品进行排序。也就是说,add 应该是 Order 类的一个方法, 而 Item 对象是一个参数。
面向对象的特点
-
将复杂的事情简单化
-
面向对象将以前的过程中的执行者,变成了指挥者
过程和对象在我们程序中是如何体现的呢? 过程其实就是函数,对象是将函数等一些内容进行了封装
-
面向对象思想符合人们思考习惯的一种思想
面向对象的三大特征
- 封装(下面会介绍)
- 继承(下面会介绍)
- 多态(下面会介绍)
自定义类
要想创建一个完整的程序, 应该将若干类组合在一起, 其中只有一个类有 main
方法。
Employee类
在Java中,最简答的类的形式如下:
class Employee {
//成员变量
field1;
field2;
....
//构造器
constructor1;
constructor2;
...
//成员方法
method1;
method2;
....
}
类的成员
在类中定义其实都称之为成员。成员有两种:
- 成员变量:其实对应的就是事物的属性
- 成员方法:其实对应的就是事物的行为
成员变量和局部变量的区别?
- 成员变量直接定义在类中
局部变量定义在方法中,参数上,语句中 - 成员变量在这个类中有效
局部变量只在自己所属的大括号内有效,大括号结束,局部变量失去作用域 - 成员变量存在于堆内存中,随着对象的产生而存在,消失而消失
局部变量存在于栈内存中,随着所属区域的运行而存在,结束而释放
构造器
构造器 用于给对象进行初始化,是给与之对应的对象进行初始化。
- 构造器与类同名,在创建类的对象时,构造器会运行。
- 构造器总会伴随
new
操作符的执行被调用。 - 每一个类可以有一个以上的构造器
- 构造器可以有
0
个、1
个或多个参数 - 构造器没有返回值
public Student {
private String name; //声明变量name,存储学生的姓名
private int age; //声明变量age,存储学生的年龄
//无参构造器
public Student() {
}
//带有一个参数的构造器
public Student(String aName) {
name = aName;
}
//带有两个参数的构造器
public Student(String aName, int aAge){
name = aName;
age = aAge;
}
}
new Student();// 使用此方法new一个对象实例会调用Student()构造器,name被初始化为null,age被初始化为0
new Student("Tom");//使用此方法new一个对象实例会调用Student(String aName)构造器,age被初始化为0
封装
定义 指隐藏对象的属性和实现细节,仅提供对外公共访问方式
// 自定义Employee类
class Employee {
// 成员变量
private String name;
private double salary;
private LocalDate hireDay;
// 构造器 或 构造函数
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
// 成员方法
// 获取姓名
public String getName() {
return name;
}
//获取薪资
public double getSalary() {
return salary;
}
//获取雇用日期
public LocalDate getHireDay() {
return hireDay;
}
/**
* 按百分比涨工资
* @param byPercent 百分比
*/
public void raiseSalary(double byPercent) {
double raise = salary * (byPercent / 100);
salary += raise;
}
}
使用private来修饰的成员变量为私有的,只能被当前类使用,体现了良好的封装性。
getName() getSalary() getHireDay()使用public来修饰,供外界来访问类的私有属性
静态变量与静态方法
静态变量
如果将变量定义为static
,每个类中只有一个这样的变量,每一个对象都共享这样一个static
变量,这个static
变量不属于任何对象,只属于这个类
public Student {
// 该静态变量只属于Student类,不管声明多少个学生对象,每个学生都共有这一个学校名。都是清华大学
private static String schoolName = "清华大学";
}
成员变量和静态变量的区别
- 成员变量所属于对象,所以也称为实例变量
静态变量所属于类,所以也称为类变量 - 成员变量存在于堆内存中
静态变量存在于方法中 - 成员变量随着对象创建而存在,随着对象被回收而消失
静态变量随着类加载而存在,随着类的消失而消失 - 成员变量只能被对象所调用
静态变量,也可以被对象调用,也可以被类调用。
静态常量
静态变量用的比较少,但静态常量用的相对比较多
例如,例如在Math类中定义一个静态常量
public Math {
private static final double PI = 3.1415926;
}
在程序中,可以使用Math.PI
的方式来使用静态常量,如果省去 static
,则必须通过 Math
的对象来访问 PI
静态方法
静态方法是一种不能向对象实施操作的方法。
public static String getSchoolName(){
return schoolName;
}
静态方法只能通过类名去访问。 example: Student.getSchoolName();
什么时候定义静态成员呢??
- 成员变量。(数据共享时静态化)
该成员变量的数据是否是所有对象都一样
如果是,那么该变量需要被静态修饰,因为是共享数据
如果不是,那么就说这是对象的特有数据,要存储到对象中 - 成员函数。(方法中没有调用特有数据时就定义静态)
如何判断成员函数是否被静态修饰呢?
只要参考,该函数内是否访问了对象中特有的数据
如果有访问特有数据,那么方法不能被静态修饰
如果没有访问特有数据,那么这个方法需要被静态修饰
重载
有些类可能有很多个构造器。例如
public Student {
private String name; //声明变量name,存储学生的姓名
private int age; //声明变量age,存储学生的年龄
//无参构造器
public Student() {
}
//带有一个参数的构造器
public Student(String aName) {
name = aName;
}
//带有两个参数的构造器
public Student(String aName, int aAge){
name = aName;
age = aAge;
}
}
这种特征叫做重载(overload)。
如果多个方法有相同的名字、不同的参数,便产生了重载。
Java允许重载任何方法,不仅仅是构造器。
不能有两个名字相同、 参数类型也相同却返回不同类型值的方法,这不是方法的重载。
初始化块
在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如:
public Student{
private String name;
private int age;
//初始化块
{
age = 18;
}
}
无论使用哪个构造器构造对象,age变量都在对象初始化块中被初始化。
首先运行初始化块,然后才运行构造器的主体部分。
对于静态成员初始化,可以使用静态初始化块
public Student{
private static String schoolName;
static{
schoolName = "清华大学";
}
}
没有显式初始化的成员变量会默认进行初始化
- 数值型默认值是
0
- 布尔型默认值是
false
- 对象引用默认值是
null
静态初始化块、初始化块、构造函数同时存在时的执行顺序:
静态初始化块 -> 初始化块 -> 构造函数
public class Demo3 {
public static void main(String[] args) {
People people = new People();
System.out.println(people.toString());
}
}
class People {
private String name;
private int age;
{
System.out.println("构造块");
}
static {
System.out.println("静态构造块");
}
public People() {
System.out.println("Person 构造器");
}
public String toString() {
return getClass().getName() + "[name=" + name + ",age=" + age + "]";
}
}
执行结果:
静态构造块
构造块
Person 构造器
People[name=null,age=0] //默认初始化值
this 与 final
this
代表当前对象。就是所在方法所属对象的引用
- 调用格式:
this(实际参数)
this
对象后面跟上.
调用的是成员变量和成员方法this
对象后面跟上()
调用的是本类中的对应参数的构造函数
final
- 这个关键字是一个修饰符,可以修饰类,方法,变量。
- 被
final
修饰的类是一个最终类,不可以被继承。 - 被
final
修饰的方法是一个最终方法,不可以被覆盖。 - 被
final
修饰的变量是一个常量,只能赋值一次。
包
Java 允许使用包( package ) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
类的导入
一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class。)
我们可以采用两种方式访问另一个包中的公有类
-
在每个类名之前添加完整的包名。
java.tiie.LocalDate today = java.tine.LocalDate.now();
-
使用
import
语句import java.util .*; LocalDate today = LocalDate.now();
在发生命名冲突的时候,就不能不注意包的名字了。例如,java.util
和 java.sql
包都有日期( Date) 类。在每个类名的前面加上完整的包名。
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date();
静态导入
import
语句不仅可以导入类,还增加了导入静态方法和静态变量的功能
import java.lang.System.*;
out.println();
将类放入包中
要想将一个类放入包中, 就必须将包的名字放在源文件的开头,包中定义类的代码之前。
package com.robin.java;
如果没有在源文件中放置 package
语句, 这个源文件中的类就被放置在一个默认包( defaulf package ) 中。
类的设计技巧
- 一定要保证数据私有(这是最重要的,要保证类的封装性)
- 一定要对数据初始化
- 不要在类中使用过多的成员变量(也就是说可以把某些变量拆为一个类,降低耦合性)
- 不是所有的成员变量都需要
get
和set
方法 - 类名和方法名要能体现具体的职责
继承
定义子类
关键字 extends
public Animal{
private int age;
public void eat(){
System.out.println("aminal eat food");
}
}
// 狗继承动物
public Dog extends Animal{
private String sex;
}
Animal称之为:超类,基类,父类
Dog称之为:子类,派生类
Dog不仅从超类中继承了age属性,而且还定义了属性sex,此时Dog类中有age,sex两个属性
eat()方法,Dog也可以同时使用
方法的覆盖
超类中的方法不一定完全适用于子类,所以需要提供一个新的方法来覆盖超类中的方法。
Animal中的eat()方法是用来吃食物,而Dog中也需要eat()方法,但是需要吃骨头,因此我们可以提供一个新的方法来覆盖超类中的方法
public void eat() {
System.out.println("Dog eat bone");
}
注意
子类在重写超类的方式时,子类方法不能低于父类方法的可见性。如超类的方法是 public
,子类一定为 public
继承的好处
- 提高代码的复用性
- 让类与类之间产生了关系,提供了另一个特征多态的前提
Java
中只支持单继承
子父类出现后,类中的成员都有了哪些特点
- 成员变量
当子父类出现一样的属性时,子类类型的对象,调用该属性,值是子类的属性值。
如果想要调用父类的属性值,需要使用一个关键字:super
this
代表是本类类型的对象引用
super
代表是子类所属父类中内存空间的引用 - 成员函数
当子父类中出现了一模一样的方法时,建立子类对象会运行子类中的方法
所以这种情况,是函数的另一个特性:覆写(重写,复写) - 构造函数
发现子类构造函数运行时,先运行了父类的构造函数。为什么呢?
原因:子类的所有构造函数的第一行,其实都有一条隐身的语句super()
super()
和 this()
是否可以同时出现在构造器中。
两个语句只能有一个定义在第一行,所以只能出现其中一个。
抽象类
如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。
在不断抽取过程中,将共性内容中的方法声明抽取,但是方法不一样,没有抽取,这时抽取到的方法,并不具体,需要被指定关键字 abstract
所标示,声明为抽象方法。
抽象类的特点
- 抽象方法只能定义在抽象类中,抽象类和抽象方法必须由
abstract
关键字修饰(可以描述类和方法,不可以描述变量) - 抽象方法只定义方法声明,并不定义方法实现
- 抽象类不可以被创建对象(实例化)
- 只有通过子类继承抽象类并覆盖了抽象类中的所有抽象方法后,该子类才可以实例化。否则,该子类还是一个抽象类
抽象类细节
- 抽象类中是否有构造函数?
有,用于给子类对象进行初始化 - 抽象类是否可以定义非抽象方法?
可以,其实,抽象类和一般类没有太大区别,都是在描述事物,只不过抽象类在描述事物时,有些功能不具体。所以抽象类和一般类在定义上,都是需要定义属性和行为的。只不过,比一般类多了一个抽象函数,而且比一般类少了一个创建对象的部分 - 抽象关键字
abstract
和哪些不可以共存?
final
private
static
- 抽象类可不可以不定义抽象方法?
可以。抽象方法目的仅仅为了不让该类创建对象
访问控制符
Java
中提供了4种访问控制符
private
- 仅对本类可见public
- 对所有类课件protected
- 对本包和所有子类可见- 默认的 - 对本包可见
Obejct - 所有类的超类
Object
类是 Java
中所有类的始祖, 在 Java
中每个类都是由它扩展而来的。但是并不需要这样写:
public class Student extends Object
equals方法
public boolean equals(Object obj) {
return (this == obj);
}
Object
类中的 equals
方法用于检测一个对象是否等于另外一个对象。在 Object
类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相等的。
Java语言规范要求 equals
方法具有以下特性 :
- 自反性:对于任何非空引用
x
,x.equals(x)
应该返回true
- 对称性: 对于任何引用
x
和y
,当且仅当y.equals(x)
返回true
,x.equals(y)
也应该返回true
- 传递性: 对于任何引用
x
,y
和z
,如果x.equals(y)
返回true
,y.equals(z)
返回true
,x.equals(z)
也应该返回true
- 一致性: 如果
x
和y
引用的对象没有发生变化,反复调用x.equals(y)
应该返回同样的结果 - 对于任意非空引用
x
,x.equals(null)
应该返回false
public boolean equals(Object otherObject){
if (this == otherObject) return true;//检测this与otherObject是否引用同一个对象
if (otherObject == null) return false;//检测otherObject是否为null,如果是返回false
if (getClass() != otherObject.getClass()) return false;//比较this与otherObject是否属于同一个类
ClassName other = (ClassName)otherObject;//将otherObject转为相应类类型变量
return field1 == other.field1 &&
Object.equals(field2, other.field2) &&
.... ; //对每项成员变量进行比较
}
hashCode方法
散列码( hash code ) 是由对象导出的一个整型值。散列码是没有规律的。
由于 hashCode
方法定义在 Object
类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址。
- 理论上对象相同,
hashcode
一定相同 hashcode
相同,对象不一定相同
toString方法
用于返回表示对象值的字符串
Object中的toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
类名@哈希值 = getClass().getName()+'@'+Integer.toHexString(hasCode())//默认格式
在自定义类中建议重写 toString
方法,用来返回类中的各个属性值
public Student{
private String name;
private int age;
public String toString() {
return getClass().getName() + "[name=" + name + ",age=" + age + "]";
}
}
Class getClass()
: 获取任意对象运行时所属字节码文件对象
String getName()
: 返回这个类的名字
继承的设计技巧
- 将公共操作和变量放在超类
- 不要使用受保护的变量
- 除非所有继承的方法都有意义,否则不要使用继承
多态
函数本身就具备多态性,某一种事物有不同的具体的体现
体现 :父类引用或者接口的引用指向了自己的子类对象。Animal a = new Cat()
多态的好处 :提高了程序的扩展性
多态的弊端 :当父类引用指向子类对象时,虽然提高了扩展性,但是只能访问父类中具备的方法,不可以访问子类中特有的方法。
多态的前提
- 必须要有关系,比如继承或者实现
- 通常会有覆盖操作
如果想用子类特有的方法,如何判断对象是哪个具体的子类类型呢?
可以通过一个关键字 instanceof
判断对象是否实现了指定的接口或继承了指定的类
格式: <对象 instanceof 类型>
判断一个对象是否所属于指定类型
Student instanceof Person == true; //Student继承了Person
多态在子父类中的成员上的体现特点
- 成员变量:在多态中,子父类成员变量同名
在编译期:参考引用型变量所属的类中是否有调用的成员(编译时不产生对象只检查语法错误)
在运行期:参考引用型变量所属的类中是否有调用的成员
成员变量 - 编译运行都看 - 左边
- 成员函数
在编译期:参考引用型变量所属的类中是否有调用方法
在运行期:参考的是对象所属的类中是否有调用方法
成员函数 - 编译看左边 - 运行看右边
- 静态函数
在编译期:参考引用型变量所属的类中是否有调用的成员
在运行期:参考引用型变量所属的类中是否有调用的成员
静态函数 - 编译运行都看 - 左边
包装类与自动装箱和拆箱
包装类
有时, 需要将 int 这样的基本类型转换为对象。 所有的基本类型都冇一个与之对应的类。
Integer 类对应基本类型 int。通常, 这些类称为包装器。
这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 和 Boolean (前 6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类。
关于包装类的具体使用,后续在常用类的文字中详细介绍
自动装箱和拆箱
当int值赋给Integer对象时,将会自动装箱
Integer i = 3;
Integer i = Integer.valueOf(3);
这种变换称之为自动装箱
当将一个Integer对象赋给一个int值时,将会自动地拆箱
Integer i = new Integer(3);
int n = i;
int n = i.intValue();
这种变化称之为自动拆箱
五大基本原则
-
开闭原则
让你的设计应当对扩展开放 ,对修改关闭 。抽象化 是开闭原则的关键。
用抽象构建框架,用实现扩展细节。
-
里氏替换原则
所有引用基类(父类)的地方必须能透明地使用其子类的对象
通俗的说:软件中如果能够使用基类对象,那么一定能够使用其子类对象。
在程序中尽量使用基类类型来对对象进行定义,在运行过程中使用子类对象。
子类可以扩展父类的功能,但不能改变父类原有的功能。
-
依赖倒置原则
要针对接口编程,不用针对实现编程。
层模块不应该依赖底层模块,他们都应该依赖抽象。抽象不应该依赖细节,细节应该依赖于抽象。 依赖三种写法: 1.构造函数注入 2.Setter依赖注入 3.接口注入 依赖原则本质:通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。 原则使用: 每个类尽量有接口或抽象类,或者抽象类和接口两者都具备 变量的类型尽量使接口或者抽象类 任何类都不应该从具体类派生 尽量不要覆写基类的方法 结合里氏替换原则
-
单一职责
在软件系统中,一个类只负责一个功能领域中的相应职责。
应该仅有一个引起它变化的原因。
该原则的核心就是解耦和增强内聚性
-
接口隔离职责
将一个接口拆分多个接口,满足不同的实现类。
总结
面向对象的思想博大精深,因此我们不仅要学会编写代码, 更更更 重要的是学会面向对象的思想。
相关代码记录于GitHub中,欢迎各位伙伴 Star !
有任何疑问 微信搜一搜 [程序猿大博] 与我联系~
如果觉得对您有所帮助,请 点赞 ,收藏 ,如有不足,请评论或私信指正,谢谢~