Java对象与类

Java对象与类

本文相关代码在我的Github,欢迎Star~

https://github.com/zhangzhibo1014/DaBoJava

前言

在此之前,我们已经把Java基础的基础语法总结了一下,今天我们来学习一下面向对象的相关知识,今天的内容理论性偏多,希望大家能耐心的看完,相信会收获很多。都说 Java 是面向对象程序设计的语言,那么究竟什么是面向对象呢?如果你没有面向对象程序设计的应用背景,那么和我一起来认真的阅读本文吧!

面向对象程序设计概述

面向对象程序设计(简称 OOP ),是当今主流的设计范型。面向对象程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。在 OOP 中,不必关心对象的具体实现,只要能满足用户的需求即可。

类( class)是构造对象的模板或蓝图。由类构造(construct) 对象的过程称为创建类的实例 (instance )。

封装( encapsulation , 有时称为数据隐藏) 是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域( instance field ), 操纵数据的过程称为方法( method ) 。对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态( state )。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。

OOP 的另一个原则会让用户自定义 Java 类变得轻而易举,这就是:可以通过扩展一个类来建立另外一个新的类。在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中,只需提供适用于这个新类的新方法和数据域就可以了。

对象

要想使用 OOP ,一定要清楚对象的三个主要特性

  • 对象的行为(behavior)- 可以对对象施加哪些操作,或可以对对象施加哪些方法?
  • 对象的状态(state)- 当施加那些方法时,对象如何响应?
  • 对象标识(identity)- 如何辨别具有相同行为与状态的不同对象?

识别类

在面向对象的学习中,我们首先要学会设计类,然后在往类中添加所需的方法。

识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。

例如:在订单处理系统中,有这样一些名词:

  • 商品(Item)
  • 订单(Order)
  • 送货地址(Shipping address)
  • 付款(Payment)
  • 账户(Account)

这些名词很可能成为类 ItemOrder 等。

接下来, 查看动词:商品被添加到订单中, 订单被发送或取消, 订单货款被支付。对于每一个动词如:“ 添加”、“ 发送”、“ 取消” 以及“ 支付”, 都要标识出主要负责完成相应动作的对象。例如,当一个新的商品添加到订单中时, 那个订单对象就是被指定的对象, 因为它知道如何存储商品以及如何对商品进行排序。也就是说,add 应该是 Order 类的一个方法, 而 Item 对象是一个参数。

面向对象的特点

  • 将复杂的事情简单化

  • 面向对象将以前的过程中的执行者,变成了指挥者

    过程和对象在我们程序中是如何体现的呢?
    过程其实就是函数,对象是将函数等一些内容进行了封装
    
  • 面向对象思想符合人们思考习惯的一种思想

面向对象的三大特征

  • 封装(下面会介绍)
  • 继承(下面会介绍)
  • 多态(下面会介绍)

自定义类

要想创建一个完整的程序, 应该将若干类组合在一起, 其中只有一个类有 main 方法。

Employee类

在Java中,最简答的类的形式如下:

class Employee {
	//成员变量
	field1;
	field2;
	....
	//构造器
	constructor1;
	constructor2;
	...
	//成员方法
	method1;
	method2;
	....
}

类的成员

在类中定义其实都称之为成员。成员有两种:

  • 成员变量:其实对应的就是事物的属性
  • 成员方法:其实对应的就是事物的行为

成员变量和局部变量的区别?

  1. 成员变量直接定义在类中

    局部变量定义在方法中,参数上,语句中

  2. 成员变量在这个类中有效

    局部变量只在自己所属的大括号内有效,大括号结束,局部变量失去作用域

  3. 成员变量存在于堆内存中,随着对象的产生而存在,消失而消失

    局部变量存在于栈内存中,随着所属区域的运行而存在,结束而释放

构造器

构造器 用于给对象进行初始化,是给与之对应的对象进行初始化。

  • 构造器与类同名,在创建类的对象时,构造器会运行。
  • 构造器总会伴随 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 = "清华大学";
}

成员变量和静态变量的区别

  1. 成员变量所属于对象,所以也称为实例变量

    静态变量所属于类,所以也称为类变量

  2. 成员变量存在于堆内存中

    静态变量存在于方法中

  3. 成员变量随着对象创建而存在,随着对象被回收而消失

    静态变量随着类加载而存在,随着类的消失而消失

  4. 成员变量只能被对象所调用

    静态变量,也可以被对象调用,也可以被类调用。

静态常量

静态变量用的比较少,但静态常量用的相对比较多

例如,例如在Math类中定义一个静态常量

public Math {
	private static final double PI = 3.1415926;
}

在程序中,可以使用Math.PI 的方式来使用静态常量,如果省去 static ,则必须通过 Math 的对象来访问 PI

静态方法

静态方法是一种不能向对象实施操作的方法。

public static String getSchoolName(){
	return schoolName;
}

静态方法只能通过类名去访问。 example: Student.getSchoolName();

什么时候定义静态成员呢??

  1. 成员变量。(数据共享时静态化)

    该成员变量的数据是否是所有对象都一样

    如果是,那么该变量需要被静态修饰,因为是共享数据

    如果不是,那么就说这是对象的特有数据,要存储到对象中

  2. 成员函数。(方法中没有调用特有数据时就定义静态)

    如何判断成员函数是否被静态修饰呢?

    只要参考,该函数内是否访问了对象中特有的数据

    如果有访问特有数据,那么方法不能被静态修饰

    如果没有访问特有数据,那么这个方法需要被静态修饰

重载

有些类可能有很多个构造器。例如

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

  1. 这个关键字是一个修饰符,可以修饰类,方法,变量。
  2. final 修饰的类是一个最终类,不可以被继承。
  3. final 修饰的方法是一个最终方法,不可以被覆盖。
  4. final 修饰的变量是一个常量,只能赋值一次。

Java 允许使用包( package ) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

类的导入

一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class。)

我们可以采用两种方式访问另一个包中的公有类

  • 在每个类名之前添加完整的包名。

    java.tiie.LocalDate today = java.tine.LocalDate.now();
    
  • 使用 import 语句

    import java.util .*;
    LocalDate today = LocalDate.now();
    

在发生命名冲突的时候,就不能不注意包的名字了。例如,java.utiljava.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 ) 中。

类的设计技巧

  • 一定要保证数据私有(这是最重要的,要保证类的封装性)
  • 一定要对数据初始化
  • 不要在类中使用过多的成员变量(也就是说可以把某些变量拆为一个类,降低耦合性)
  • 不是所有的成员变量都需要 getset 方法
  • 类名和方法名要能体现具体的职责

继承

定义子类

关键字 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 中只支持单继承

子父类出现后,类中的成员都有了哪些特点

  1. 成员变量

    当子父类出现一样的属性时,子类类型的对象,调用该属性,值是子类的属性值。

    如果想要调用父类的属性值,需要使用一个关键字: super

    this 代表是本类类型的对象引用

    super 代表是子类所属父类中内存空间的引用

  2. 成员函数

    当子父类中出现了一模一样的方法时,建立子类对象会运行子类中的方法

    所以这种情况,是函数的另一个特性:覆写(重写,复写)

  3. 构造函数

    发现子类构造函数运行时,先运行了父类的构造函数。为什么呢?

    原因:子类的所有构造函数的第一行,其实都有一条隐身的语句 super()

super()this() 是否可以同时出现在构造器中。 两个语句只能有一个定义在第一行,所以只能出现其中一个。

抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

在不断抽取过程中,将共性内容中的方法声明抽取,但是方法不一样,没有抽取,这时抽取到的方法,并不具体,需要被指定关键字 abstract 所标示,声明为抽象方法。

抽象类的特点

  1. 抽象方法只能定义在抽象类中,抽象类和抽象方法必须由 abstract 关键字修饰(可以描述类和方法,不可以描述变量)
  2. 抽象方法只定义方法声明,并不定义方法实现
  3. 抽象类不可以被创建对象(实例化)
  4. 只有通过子类继承抽象类并覆盖了抽象类中的所有抽象方法后,该子类才可以实例化。否则,该子类还是一个抽象类

抽象类细节

  1. 抽象类中是否有构造函数?

    有,用于给子类对象进行初始化

  2. 抽象类是否可以定义非抽象方法?

    可以,其实,抽象类和一般类没有太大区别,都是在描述事物,只不过抽象类在描述事物时,有些功能不具体。所以抽象类和一般类在定义上,都是需要定义属性和行为的。只不过,比一般类多了一个抽象函数,而且比一般类少了一个创建对象的部分

  3. 抽象关键字abstract和哪些不可以共存?
    final private static

  4. 抽象类可不可以不定义抽象方法?

    可以。抽象方法目的仅仅为了不让该类创建对象

访问控制符

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 方法具有以下特性

  • 自反性:对于任何非空引用 xx.equals(x) 应该返回 true
  • 对称性: 对于任何引用 xy,当且仅当 y.equals(x) 返回 truex.equals(y) 也应该返回 true
  • 传递性: 对于任何引用 xyz ,如果 x.equals(y) 返回 truey.equals(z) 返回 truex.equals(z) 也应该返回 true
  • 一致性: 如果 xy 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果
  • 对于任意非空引用 xx.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()

多态的好处 :提高了程序的扩展性

多态的弊端 :当父类引用指向子类对象时,虽然提高了扩展性,但是只能访问父类中具备的方法,不可以访问子类中特有的方法。

多态的前提

  1. 必须要有关系,比如继承或者实现
  2. 通常会有覆盖操作

如果想用子类特有的方法,如何判断对象是哪个具体的子类类型呢?

可以通过一个关键字 instanceof 判断对象是否实现了指定的接口或继承了指定的类

格式: <对象 instanceof 类型> 判断一个对象是否所属于指定类型

Student instanceof Person == true; //Student继承了Person

多态在子父类中的成员上的体现特点

  1. 成员变量:在多态中,子父类成员变量同名

    在编译期:参考引用型变量所属的类中是否有调用的成员(编译时不产生对象只检查语法错误)

    在运行期:参考引用型变量所属的类中是否有调用的成员

    成员变量 - 编译运行都看 - 左边

  2. 成员函数

    在编译期:参考引用型变量所属的类中是否有调用方法

    在运行期:参考的是对象所属的类中是否有调用方法

    成员函数 - 编译看左边 - 运行看右边

  3. 静态函数

    在编译期:参考引用型变量所属的类中是否有调用的成员

    在运行期:参考引用型变量所属的类中是否有调用的成员

    静态函数 - 编译运行都看 - 左边

包装类与自动装箱和拆箱

包装类

有时, 需要将 int 这样的基本类型转换为对象。 所有的基本类型都冇一个与之对应的类。

Integer 类对应基本类型 int。通常, 这些类称为包装器。

这些对象包装器类拥有很明显的名字:IntegerLongFloatDoubleShortByteCharacterBoolean (前 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

有任何疑问 微信搜一搜 [程序猿大博] 与我联系~

如果觉得对您有所帮助,请 点赞收藏 ,如有不足,请评论或私信指正,谢谢~