面向对象程序设计概述
面向对象程序设计(简称OOP)是当今主流的程序设计范型,Java是
完全面向对象
的语言。
在OOP(Object Oriented Programming)中,不必关心对象的具体实现,只要能满足用户需求即可。 采用面向对象的方式进行开发:耦合度低,扩展力强
。
文章目录
一、类与对象
类实际上在现实世界当中是不存在的,是一个抽象的概念,是一个
模板
,是我们人类大脑进行“思考、总结、抽象”的一个结果,类本质上是现实世界当中某些事物具有共同特征
,将这些共同特征提取出来形成的概念就是一个“类”。而对象是实际存在的个体,对象还有另一个名字叫做
实例
。类是构造对象的
模板
或蓝图
,通过类这个模板创建对象的过程,叫做:实例化
。
1.1 类之间的关系
在类之间,最常见的关系有:
(一)继承 / 泛化关系
,即"is-a"关系。
用来描述继承关系,在 Java 中使用 extends 关键字。
(二)实现关系
。
用来实现一个接口,在 Java 中使用 implements 关键字。
(三)聚合关系
,即"has-a"关系。
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
(四)依赖关系
,即"uses-a"关系。
例如,A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类方法的局部变量;
- A 类是 B 类方法的参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化。
1.2 对象构造
要想使用对象,就必须首先构造对象,并指定其初始状态(实例变量)。什么是实例变量?
对象又被称为实例。实例变量实际上就是:对象级别的变量
。
public class student{
String name;
double height;
boolean sex;
}
名字、身高、性别这些属性所有的学生都有,但是每一个对象都有“自己的名字、性别、身高值”。
假设创建10个学生对象,name、height、sex变量应该有10份。
所以这种变量被称为对象级别的变量,属于实例变量。
在Java程序设计语言中,使用构造器构造新实例。构造器是一种特殊的方法,编写在类当中,用来构造并初始化对象。
public static void main(String[] args){
Student zhangsan = new Student(); // 其中Student()为无参构造器
}
构造器的名字应该与类名相同。因此,Student类的构造器名为Student。在构造器前面加上new操作符,即可构造一个Student对象。
注意:
这里的对象变量zhangsan并没有实际包含一个对象,而仅仅引用一个对象
。
对象和引用的区别?
对象是通过new出来的,在堆内存中存储。
引用是:但凡是变量,并且该变量中保存了内存地址指向了堆内存当中的对象的。
无参构造器
public Student(){
}
由无参构造器创建出来的对象,其状态(实例变量)会设置为不同数据类型对应默认值。
即:
public Student(){
name = ""; // 即为 null
height = 0.0;
sex = false;
}
方法重载
多个方法有相同的名字、不同的参数,便产生了重载。这样的机制缓解了程序员苦想方法名的鸭力。
(方法重载只与参数的类型,参数的个数,和参数的顺序有关,与返回值类型无关)
有参构造器便是其中的例子。
有参构造器
public Student(String name,double height,boolean sex){
this.name = name;
this.height = height; // 这里的this表示当前对象
this.sex = sex;
}
此时,编译器通过有参构造方法给出的参数类型与调用方法时传入的参数类型进行匹配,如果不匹配则编译时报错。
1.3 封装
面向对象的三大特征:
封装
、继承
、多态
封装使数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。(一般提供提供一个公有的访问getter方法,和一个公有的更改setter方法)
public class Student{
private double height;
public boolean getHeight() {
return height;
}
public void setHeight(boolean height) {
this.height = height;
}
}
还可以在setter方法中编写代码来执行错误检查,避免一些错误修改。例如:将学生对象的身高设置为负数。
1.4 final与static
final
对于基本类型,final 使数值不变;
对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final定义的变量必须显示初始化。
final int x = 1;
// x = 2; // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
final声明的方法不能被子类重写。
final声明的类不允许被继承。
static
静态变量
当同一个类创建出来的对象中都含有一个相同值的变量时,往往将他们定义为静态static。
如一个中国人类中,国籍属性都为中国,所以国籍就应该被定义为静态。
public class Chinese{
private int id;
static String country = china;
public static void eat(){}
}
类所有的实例都共享静态变量,静态变量在内存中只存在一份。
换句话来说,如果有100个Chinese类的对象,则有100个实例变量id,但是只有一个静态变量country,并且值都为china。即使没有一个chinese对象存在,静态变量country也存在。它属于类,不属于任何独立的对象。
静态方法
静态方法是一种不能向对象实施操作的方法。并且可以认为静态方法是没有this参数的方法。
一般用 类名点 来调用静态方法,不需要用对象调用静态方法。
public static void main(String[] args){
chinese.eat();
}
静态方法在类加载的时候就存在了,它不依赖于任何实例。
同理,这里的main方法也是一个静态方法。main方法不对任何对象进行操作。事实上,在启动程序时还没有任何对象。静态的main方法将执行并创建程序所需要的对象。
静态语句块
静态语句块在类初始化时运行一次。
public class Chinese{
static{
System.out.println("类初始化时运行一次");
}
public static void main(String[] args){
Chinese a = new Chinese();
}
}
在初始化的顺序中,静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
二、继承
利用
继承
,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和实例变量。在此基础上,还可以添加一些新的方法和实例变量,以满足新的需求。
2.1 父类和子类
继承的相关特性
① B类继承A类,则称A类为 超类(superclass)、父类、基类
,
B类则称为子类(subclass)、派生类、扩展类
。
class A{}
class B extends A{}
② java 中的继承只支持单继承,不支持多继承,C++中支持多继承,
这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码:
//class B extends A,C{ } // error
③ 虽然 java 中不支持多继承,但有的时候会产生间接继承的效果,
例如:
class C extends B{}
class B extends A{}
也就是说,C 直接继承 B,
其实 C 还间接继承 A。
④ java 中规定,子类继承父类,除构造方法
不能继承之外,剩下都可以继承。
但是私有的属性
无法在子类中直接访问。(父类中private修饰的不能在子类中
直接访问。可以通过间接的手段来访问。)
⑤ java 中的类没有显示的继承任何类,则默认继承 Object类
,Object类是
java 语言提供的根类(超类),也就是说,一个对象与生俱来就有
Object类型中所有的特征。
⑥ 继承也存在一些缺点,例如:CreditAccount 类继承 Account 类会导致它
们之间的耦合度非常高,Account 类发生改变之后会马上影响到 CreditAccount 类
方法覆盖
父类中的方法无法满足子类的业务需求,子类对继承过来的方法进行
覆盖/重写
。
满足方法覆盖的条件:
第一:有继承关系的两个类
第二:具有相同方法名、返回值类型、形式参数列表
第三:访问权限不能更低。
第四:抛出异常不能更多。
私有方法无法覆盖。
方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。(这是因为方法覆盖通常和多态联合起来)
super关键字
super关键字的两个用途:1.调用父类的方法 2.调用父类的构造器
所以super能出现在实例方法和构造方法中。
super的语法是:“super.”、“super()”
super不能使用在静态方法中。
super. 大部分情况下是可以省略的。
super.什么时候不能省略呢?
父类和子类中有同名属性,或者说有同样的方法,想在子类中访问父类的,super. 不能省略。
super() 只能出现在构造方法第一行,通过当前的构造方法去调用“父类”中的构造方法,目的是:创建子类对象的时候,先初始化父类型特征。如果子类的构造器没有显式调用父类的构造器,则将自动调用父类的无参构造器。如果父类没有无参构造器,并且子类构造器中没有显式地调用父类的其他构造器,则编译器报错。
class Animal{
int age;
public Animal(int age){
this.age = age;
}
}
class cat extends Animal{
public cat(int age){
super(age);
}
}
class dog extends Animal{
public dog(int age){
super(age);
}
}
2.2 多态
一个对象变量可以指示多种实际类型的现象被称为
多态
(polymorphism)。在运行时能够自动地选择调用哪个方法地现象称为动态绑定
(dynamic binding)。
多态在开发中的作用是:降低程序的耦合度
,提高程序的扩展力
。
例如下面这个例子:
public class Master{
public void feed(Dog d){
System.out.println("狗吃骨头");
}
public void feed(Cat c){
System.out.println("猫吃鱼");
}
}
class Dog{}
class Cat{}
以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力
很差。
public class Master{
public void feed(Pet pet){
pet.eat();
}
public static void main(String [] args){
Master m = new Master();
// 向上转型 Pet p1 = new Dog();
m.feed(new Dog()); // 狗吃骨头
// 向上转型 Pet p2 = new Cat();
m.feed(new Cat()); // 猫吃鱼
}
}
class Pet{
public void eat(){}
}
class Dog extends Pet{
@Override
public void eat(){
System.out.println("狗吃骨头");
}
}
class Cat extends Pet{
@Override
public void eat(){
System.out.println("猫吃鱼");
}
}
以上的代表中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类(指Dog、Cat类的父类)。
这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性
。
向上转型和向下转型
向上转型
:子—>父 (upcasting)
又被称为自动类型转换:Pet p1 = new Dog();
向下转型
:父—>子 (downcasting)
又被称为强制类型转换:Dog c = (Dog)p1;
需要添加强制类型转换符。
什么时候需要向下转型?
需要调用或者执行子类对象中特有的方法,必须进行向下转型,才可以调用。
编译时
多态主要指方法的重载
运行时
多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
Pet p1 = new Dog();
// 编译的时候编译器发现p1的类型是Pet,所以编译器会去Pet类中找eat()方法
// 找到了,绑定,编译通过。但是运行的时候和底层堆内存当中的实际对象有关
// 真正执行的时候会自动调用“堆内存中真实对象”的相关方法。
m.feed();
多态的典型代码:父类型的引用指向子类型的对象。
2.3 抽象类
例如,People、Student、Teacher这三个类,从我们使用的角度来看主要对 Student 和Teacher 进行实例化,Person 中主要包含了一些公共的属性和方法,而 Person 我们通常不会实例化,所以我们可以把它定义成抽象的:
抽象类
和抽象方法
都使用 abstract
关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。
如果这个类是抽象的,那么这个类被子类继承,抽象方法必须被重写。如果在子类中不复写该抽象方法,那么必须将此类再次声明为抽象类。
public abstract class People{
private String name;
protected boolean sex;
public abstarct void write(){}
}
public class Student extends People{
public Student(String name,boolean sex){
super(name);
this.sex = sex;
}
@Override
public void write(){
System.out.println("学生写字");
}
}
2.4 Object类
Object类是Java中所有类的根基,在Java中每个类都是由它扩展而来的。如果在类的声明中未使用 extends 关键字指明其基类,则默认基类为 Object 类。
等号“==”与equals方法
等号可以比较基本类型和引用类型,等号比较的是值,特别是比较引用类型,比较的是引用的
内存地址,但对于多数类来说,这种判断没有什么意义。
默认的equals方法也是判断两个对象是否具有相同的引用,虽然合理,同样也意义不大,所以通常我们会在子类中覆盖equals方法。
两个对象具有等价关系,需要满足以下五个条件:
①自反性
x.equals(x); // true
②对称性
x.equals(y) == y.equals(x); // true
③传递性
if (x.equals(y) && y.equals(z))
x.equals(z); // true;
④一致性
多次调用equals方法结果不变
x.equals(y) == x.equals(y); // true
⑤对任何非空引用x,x.equals(null)应该返回false
实现建议:
1.检查是否为同一个对象的引用,如果是直接返回 true;
2.检查是否是同一个类型,如果不是,直接返回 false;
3.将 Object 对象进行转型;
4.判断每个关键域是否相等。
public class EqualExample {
private int x;
private int y;
private int z;
public EqualExample(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EqualExample that = (EqualExample) o;
if (x != that.x) return false;
if (y != that.y) return false;
return z == that.z;
}
}
• 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getclass进行检测。
• 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的判断。
hashCode方法
hashCode()
返回哈希值,而 equals()
是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。
在覆盖 equals()
方法时应当总是覆盖 hashCode()
方法,保证等价的两个对象哈希值也相等。
(到集合类再补充说明)
toString方法
返回该对象的字符串表示。通常 toString 方法会返回一个“以文本方式表示”此对象的字符串,Object 类的 toString 方法返回一个字符串,该字符串由类名加标记@和此对象哈希码的无符号十六进制表示组成,Object 类 toString 源代码如下:
getClass().getName() + '@' + Integer.toHexString(hashCode())
在进行 String 与其它类型数据的连接操作时,如:
System.out.println(student);
,它自动调用该对象的 toString()
方法。
三、接口
在Java程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。接口我们可以看作是
抽象类的一种特殊情况
,在接口中只能定义抽象的方法和常量
3.1 接口的特性
- 在 java 中接口采用
interface
声明
public interface Comparable{}
- 接口中的方法默认都是
public abstract
(一般不需要手动标记) ,不能更改。 - 接口中的变量默认都是
public static final
类型的,不能更改,所以必须显示的初始化 - 从 Java 8 开始,接口也可以拥有默认的方法实现(用default修饰符标记方法),这是因为不支持默认方法的接口的维护成本太高了,而默认方法不需要实现类去实现。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,让它们都实现新增的方法。
public interface Comparable{
public static final int a = 1;
public static final String b = "哈哈";
int compareTo(Employee other){
return 0;
}
default void func1(){
System.out.println("默认方法");
}
}
- 接口不能被实例化,接口中没有构造函数的概念
- 接口之间可以继承,但接口之间不能实现
- 接口中的方法只能通过类来实现,通过
implements
关键字 - 如果一个类实现了接口,那么接口中所有的方法必须实现
class Student implements Comparable{
@Override
int compareTo(Employee other){
System.out.println("实现、覆盖方法");
return 1;
}
}
- 一类可以实现多个接口
class Student implements Comparable,Person,Named{}
3.2 接口与抽象类的区别
a)从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
b)接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
c) 接口描述了方法的特征,不给出实现,一方面解决 java 的单继承问题,实现了强大的可接插性
d) 抽象类提供了部分实现,抽象类是不能实例化的,抽象类本身不能被使用者直接实例化,但被具体类继承后,就可以在具体类实例化的过程中被实例化了,而接口是完全不能实例化的。抽象类的存在主要是可以把公共的代码移植到子类中。
e) 面向接口编程,而不要面向具体编程(面向抽象编程,而不要面向具体编程)
f) 优先选择接口(因为继承抽象类后,此类将无法再继承,所以会丧失此类的灵活性,而且JAVA语言中对接口的限制可以避免因类继承而引起的所有问题)
四、内部类
内部类(inner class)是定义在另一个类中的类。内部类主要分类:
实例内部类
局部内部类
静态内部类
• 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据
• 内部类可以对同一个包中的其他类隐藏起来
实例内部类
创建实例内部类,外部类的实例必须已经创建
实例内部类会持有外部类的引用
实例内部不能定义 static 成员,只能定义实例成员
public class InnerClassTest01 {
private int a;
private int b;
InnerClassTest01(int a, int b) {
this.a = a;
this.b = b;
}
//内部类可以使用 private 和 protected 修饰
private class Inner1 {
int i1 = 0;
int i2 = 1;
int i3 = a;
int i4 = b;
//实例内部类不能采用 static 声明
//static int i5 = 20;
}
public static void main(String[] args) {
InnerClassTest01.Inner1 inner1 = new InnerClassTest01(100, 200).new Inner1();
System.out.println(inner1.i1);
System.out.println(inner1.i2);
System.out.println(inner1.i3);
System.out.println(inner1.i4);
}
}
静态内部类
静态内部类不会持有外部的类的引用,创建时可以不用创建外部类
静态内部类可以访问外部的静态变量,如果访问外部类的成员变量必须通过外部类的实例访问
public class InnerClassTest02 {
static int a = 200;
int b = 300;
static class Inner2 {
//在静态内部类中可以定义实例变量
int i1 = 10;
int i2 = 20;
//可以定义静态变量
static int i3 = 100;
//可以直接使用外部类的静态变量
static int i4 = a;
//不能直接引用外部类的实例变量
//int i5 = b;
//采用外部类的引用可以取得成员变量的值
int i5 = new InnerClassTest02().b;
}
public static void main(String[] args) {
InnerClassTest02.Inner2 inner = new InnerClassTest02.Inner2();
System.out.println(inner.i1);
}
}
局部内部类
局部内部类是在方法中定义的,它只能在当前方法中使用。和局部变量的作用一样
局部内部类和实例内部类一致,不能包含静态成员
public class InnerClassTest03 {
private int a = 100;
//局部变量,在内部类中使用必须采用 final 修饰
public void method1(final int temp) {
class Inner3 {
int i1 = 10;
//可以访问外部类的成员变量
int i2 = a;
int i3 = temp;
}
//使用内部类
Inner3 inner3 = new Inner3();
System.out.println(inner3.i1);
System.out.println(inner3.i3);
}
public static void main(String[] args) {
InnerClassTest03 innerClassTest03 = new InnerClassTest03();
i nnerClassTest03.method1(300);
}
}
匿名内部类
是一种特殊的内部类,该类没有名字
• 没有使用匿名类
public class InnerClassTest04 {
public static void main(String[] args) {
MyInterface myInterface = new MyInterfaceImpl();
myInterface.add();
}
}
interface MyInterface {
public void add();
}
class MyInterfaceImpl implements MyInterface {
public void add() {
System.out.println("-------add------");
}
}
• 使用匿名类
public class InnerClassTest05 {
public static void main(String[] args) {
InnerClassTest05 innerClassTest05 = new InnerClassTest05();
innerClassTest05.method1(new MyInterface() {
public void add() {
System.out.println("-------add------");
}
});
}
private void method1(MyInterface myInterface) {
myInterface.add();
}
}
interface MyInterface {
public void add();
}
本文参考文献:
① Java核心技术卷一(第十版)
② https://github.com/CyC2018/CS-Notes
写在最后:新手上路,如有问题,欢迎大家指出,给意见。如果这篇文章对你有帮助,麻烦帮忙点一下赞,你们小小的举动能给我无限大的动力。拒绝白嫖,从我做起,从现在做起。Thank you for watching!