Java 基础08-OOP面向对象编程

240 阅读21分钟

面向对象编程 OOP

1. 入门便是不识

从我们刚开始接触到 Java 这门语言之后,除了所谓的增删改查,就是会有人告诉我们这是一个面向对象编程的语言,然而说道面向对象,听到最多的就是"不就对象嘛,new 一个不就行了!",其实并不知道什么是对象。

现在就赶紧回来看看什么是 OOP 面向对象编程(Object Oriented Programming)。

2. 举例说明什么是面向对象

举个最简单点的例子来区分,面向过程和面向对象。

有一天你想吃鱼香肉丝了,怎么办呢?你有两个选择:
	1、自己买材料,肉,鱼香肉丝调料,蒜苔,胡萝卜等等然后切菜切肉,开炒,盛到盘子里。
	2、去饭店,张开嘴:老板!来一份鱼香肉丝!

看出来区别了吗?在这里 1 是面向过程,2 是面向对象。

面向对象有什么优势呢?首先你不需要知道鱼香肉丝是怎么做的,降低了耦合性。如果你突然不想吃鱼香肉丝了,想吃烤羊排,对于 1 你可能不太容易了,还需要重新买菜,买调料什么的。对于 2的话太容易了,大喊:老板!那个鱼香肉丝换成烤羊排吧!提高了可维护性。总的来说就是降低耦合,提高维护性!

面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。

面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。

面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们我们使用的就是面向对象了。

2.1 优缺点分析

上面举例描述了什么是面向过程和面向对象,接下来我们总结一下各自的特点。

对于面向过程:
	优点:性能比面向对象好,因为调用时需要实例化,开销比较大,比较消耗资源。
	缺点:不易维护、不易复用、不易扩展。
	
对于面向对象:
	优点:易维护、易复用、易扩展,由于面向对象有封装、多态、继承的特性,可以设计出低耦合的系统,使系统更加灵活、更加易用维护。
	缺点:性能方面比面向过程差。

3. 面向对象的三大特征

面向对象有三大特征,分别是封装、多态和继承。接下来我们来逐个来学习一下。

那么首先我们就要举个例子,对象。

对象就是对事物的一种抽象描述。现实世界中的事物,都可以用"数据""能力"来描述。

比如我要描述一个人,"数据"就是他的年龄、性别、身高体重,"能力"就是他能做什么工作,承担什么样的责任。

描述一个外卖软件,"数据"就是他包含的菜品,而"能力"就是他可以点菜。

3.1 面向对象特征-封装

隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。

我们把"数据""能力"组合成一个对象的过程就叫做"封装"。
    
封装的结果就是可以有一个类,通过这个类我们可以获得一个对象。然后我们就可以通过给这个对象下达指令,然后让他执行自己的"能力"。

封装只是面向对象的第一步,目的是把现实世界的东西抽象成对象。面向对象真正有威力的地方是"多态""继承"。
    
封装的核心思想是:隐藏实现,暴露接口。
    
封装的目的:
    1、解耦
    2、保护数据安全
3.1.1 访问修饰权限符
修饰符 权限范围
public 本类,子类,本包,外部包
protected 本类,子类,本包
default 本类,子类
private 本类

3.2 面向对象特征-多态

父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。

同一操作,作用于不同的对象,可以产生不同结果,这就是"多态"。通常说的多态是指运行期的多态,也叫动态绑定。

要实现多态,需要满足三个条件:
	1、有类继承或接口实现;
	2、子类重写父类的方法;
	3、父类的引用指向子类的对象。
	
比如:
	犬科动物 {
		吠();
	}
	
	狗 继承 犬科动物 {
        吠(){
            汪汪汪!
        }
    }

	狼 继承 犬科动物 {
        吠(){
            嗷嗷嗷!
        }
    }

狗和狼都是犬科动物,拉来一只犬科动物,如果让你来分辨的话,你可能没办法直接分辨出它到底是狗还是狼。只要它真正的叫出来的时候,你才知道。这就是运行是多态。

转化成 Java 代码如下:
	public class Canidae {
		
		public void bark(){};
	}
	
	public class Dog extends Canidae {
		
        public void bark() {
			
            System.out.println("汪汪汪...")
		}
	}
	
	public class Wolf extends Canidae {
		
        public void bark() {
			
            System.out.println("嗷嗷嗷...")
		}
	}
	
	public class Main() {
        
		Canidae canidae = new Dog();
		Canidae canidae1 = new Wolf();
        
         canidae.bark();
         canidae1.bark();
	}

这样,就实现了多态,同样的是 Canidae 的实例,canidae.bark() 调用的就是 Dog 类的方法,而 canidae1.bark() 调用的却是 Wolf 的方法。
3.2.1 重写和重载
"重写"指子类重写父类的方法,方法名、参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 或者 final 则子类就不能重写方法。
    
"重载"发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
3.2.2 向上转型
在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。
3.2.3 强制转换
从子类到父类的类型转换可以自动进行。
    
从父类到子类的类型转换必须通过造型(强制类型转换)实现。
    
无继承关系的引用类型间的转换是非法的。

3.3 面向对象特征-继承

提高代码复用性;继承是多态的前提。

在面向对象编程中,当两个类具有相同的特征(属性)和行为(方法)时,可以将相同的部分抽取出来放到一个类中作为父类,其他两个类"继承"这个父类。继承后子类自动拥有了父类的部分属性和方法。

通过继承创建新类称为"子类""派生类"。

被继承的类称为"基类""父类""超类"。

比如:
	狗 {
        吠();
    }

	牧羊犬 继承 狗 {
        放羊();
    }

上面的例子中,狗类时父类,牧羊犬是子类。牧羊犬类通过继承获得狗类的 吠() 的能力,同时增加了自己独有的 放羊() 的能力。

转换成 Java 代码如下:
	public class Dog {
        
        public void bark() {
           
            System.out.println("汪汪汪...")
        }
    }

	public class HerdingDog extends Dog {
       
        public void herd() {
           
            System.out.println("放羊中...")
        }
    }

	public class Main() {
       
        HerdingDog dog = new HerdingDog();
        
        dog.bark();
        dog.herd();
    }
3.3.1 构造器的继承问题
1、构造器是不会被子类继承的,但是子类的对象在初始化时会默认调用父类的无参构造器
    
2、当父类显示写了有参构造器,且没有无参构造器,子类继承父类的时候必须显示的调用父类的有参构造器。调用的方式可以使用 super(a,b) 来调用。
3.3.2 static 修饰符的继承问题
子类是不会继承父类被 static 修饰的方法和变量,但是可以调用。
3.3.3 super 关键字
super 关键字用于引用使用该关键字的类的父类。

作为独立语句出现的 super 表示调用父类的构造方法。

如果是为了调用父类的成员变量,成员方法是可以直接使用的,不需要借助于 super。

如果子类中出现了和父类同名的成员变量,成员方法,可以使用 super 关键字告知编译器,这里调用的是父类的成员变量,成员方法。

super关键字调用构造方法,父类的构造方法
	
    格式:
		super(实际参数);

		1. super 调用父类构造方法,是通过参数类型,个数和顺序来确定对应的父类构造方法;

		2. super 调用父类构造方法,必须在当前子类构造方法代码块的第一行;

		3. thissuper 不能同时出现在同一个子类构造方法中调用构造方法;

		4. Java 编译器会自动选择"隐式"调用 super() 对应的就是父类的无参构造方法,用于初始化父类的成员变量内存空间。
3.3.4 this 关键字
this 关键字用于引用当前实例。

当引用可能不明确时,可以使用 this 关键字来引用当前的实例。

表示调用当前方法的类对象。

可以利用 this 调用类对象的成员变量和成员方法,可以用于操作当前类对象。

this 关键字调用构造方法:
	
	格式:
		this(实际参数);

	1、有且只能用在 Constructor 构造方法内;
	
	2this() 在类内调用其他构造方法,是根据小括号内的实际参数类型来选择的;
	
	3、两个构造方法,不能通过 this 关键字,相互调用,出现语法错误;
	
	4this(实际参数)调用其他构造方法,有且只能是在当前构造方法代码块的第一行,并且只能调用一个。
3.3.5 final 关键字
final 修饰类:
	不能被继承,例如 String

final 修饰成员方法:
	不能被子类或者实现类重写

final 修饰成员变量:
	定义时必须初始化,并且初始化之后无法修改

final 修饰局部变量:
	赋值之后无法修改
        
final 标记的变量(成员变量或局部变量)即称为常量。名称大写,且只 能被赋值一次。
3.3.6 子类对象实例化过程
new 出一个子类对象的时候,必须先 new 出一个父类对象。子类在调用构造方法时,会优先调用父类的构造方法。(默认)
    
如果不写构造方法,编译器默认加上一个无参构造器,如果写了构造器,编译器就不会添加。
    
如果写了有参构造器,子类就必须调用父类的构造方法。super(参数)。
    
如果同时有有参和无参构造器,那么会默认调用无参。也可以自定义调用有参。

3.4 简单小结

1、抽象会使复杂的问题更加简化。
2、从以前面向过程的执行者,变成了面向对象的指挥者。
3、面向对象更符合人类的思维,面向过程则是机器的思想。

4. Object 类

4.1 Object 类

1、Object 类时所有 Java 的根父类
    
2、如果在类的声明未使用 extends 关键字指名其父类,则默认父类为 java.lang.Object 类(任何类都可以调用 Object 的方法)

Object 的主要组成:
    1public native int hashCode()
    	取得 hash 码
    2、equals(Object obj)
		比较对象
    3、clone()
		可用于复杂对象的深拷贝            

4.2 == 与 equals 的区别

== 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址。
    
equals 方法用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object 类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,而 Object 中的 equals 方法返回的却是 == 的判断。

5. static 关键字

在 Java 类中,可用 static 修饰属性、方法、代码块、内部类。

被修饰后的成员具备以下特点:
	1、修饰的成员,被所有对象所共享;
	2、访问权限允许时,可不创建对象,直接用类名.属性或方法调用。

在 static 方法内部只能访问类的 static 修饰的属性或方法,不能访问类的非 static 的结构。

static 修饰的方法不能被重写。
        

static 修饰成员变量
	1、保存在内存的数据区;
	2、生存周期是从类文件加载开始,到程序退出结束,生存周期是远远超过类对象的;
	3、一处修改,所有使用到该成员变量的位置都会被影;
	4、调用格式:
		类名.静态成员变量 √
		
static 修饰成员方法
	1、调用格式:
		类名.静态成员方法(); √
	2、生存周期是从类文件加载开始,到程序退出结束,整个生存周期超过了类对象;
	3、在静态成员方法中不允许使用非静态成员;
		a. 不能直接使用非静态成员变量
		b. 不能直接使用非静态成员方法
		c. 不能直接使用 this 关键字
	4static 修饰的静态成员方法一般用于封装成工具类。
	
static 修饰静态代码块
	类文件加载时,一定会执行 static 修饰的静态代码块 
	1、程序加载过程中,完成一些启动必须项  MySQL JDBC 加载驱动;
	2、可以在程序加载过程中,完成一些程序运行必须的数据 MySQL 连接数据库的必须数据。

6. 代码块

对类或对象进行初始化。
    
代码块可分为静态代码块和非静态代码块。(有无 static 修饰)
    
静态代码块:
    用 static 修饰的代码块。 
    1、可以有输出语句;
    2、可以对类的属性、类的声明进行初始化操作;
    3、不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法;
    4、若有多个静态的代码块,那么按照从上到下的顺序依次执行;
    5、静态代码块的执行要先于非静态代码块;
    6、静态代码块随着类的加载而加载,而且值执行一次。
  
        
非静态代码块:
        没有 static 修饰的代码块。
        1、可以用输出语句;
        2、可以对类的属性、类的声明进行初始化操作;
        3、除了调用非静态的结构外,还可以调用静态的变量和方法;
        4、若有多个非静态的代码块,那么按照从上到下的顺序依次执行;
        5、每次创建对象的时候,都会执行一次。且先于构造器执行。

7. 抽象类和抽象方法

7.1 什么是抽象类和抽象方法

abstract 关键字来修饰一个类,这个类叫做抽象类
    
用 abstract 关键字来修饰一个方法,该方法叫做抽象方法。

7.2 abstract 关键字

abstarct 关键字修饰的成员方法,是要求子类强制重写的。

1abstract 修饰的方法没有方法体;
2abstract 修饰的方法必须定义在 abstract 修饰的类内或者 interface 接口内;
3、一个普通类继承于 abstract 修饰的类,需要完成实现在 abstract 类内的所有 abstract 方法;
4abstract 类没有自己的类对象。

8. interface

1、用 interface 来定义;
2、接口中的所有成员变量都默认是由 public static final 修饰的;
3、接口中的所有抽象方法都默认由 public abstract 修饰的;


interface 关键字使用来声明新的 Java 接口,接口是方法的集合。

接口是 Java 语言的一项强大功能。
    
任何类都可以声明它实现一个或多个接口,这意味着它实现了在这些接口中所定义的所有方法。
 
实现了接口的任何类都必须提供在该接口中的所有方法的实现。
    
一个类可以实现多个接口。

9. 接口和抽象类的区别

相同点:
    1、都不能创建对象;
    2、都可以定义抽象方法,并且一定在子类中重写。

        
不同点:
    1、关键字不同 interfaceabstract;
    2、抽象方法中既可以有抽象方法也可以有普通方法;
    3、接口中所有的方法都是抽象方法;
    4、抽象类的方法可以任意权限,接口中的方法只能是 public;
    5、抽象类只能单继承,接口可以多实现。

        
JDK 1.8 开始,接口中的方法不再是只能有抽象方法(普通方法会被隐式地指定为 public abstract 方法),它还可以有静态方法和 default 方法。并且静态方法与 default 方法可以有方法体!
        
让我们来看一下下面的例子:
        
public interface NewInterface {
        
        static void staticMethod() {
            
            System.out.println("staticMethod");
        }
        
        default void defaultMethod() {
            
            System.out.println("defaultMethod");
        }
        
        public void getInfo();
    }

由上面的例子可以看出给出一个接口,在 JDK 1.8 的环境下,他可以拥有静态方法和 default 方法,所谓 default 方法即是使用 default 关键字来修饰的方法。
    
一个接口可以有多个静态方法和 default 方法,没有个数限制。实现类只需要实现它的抽象方法即可。
    
关于静态方法和 default 方法的调用:
    1、对于静态方法,直接由接口名调用,不需要由接口实现类的对象来调用;
    2、对于 default 方法,很明显是需要实例对象来调用的。


import org.junit.Test;
 
public class NewInterfaceTest {
    
    @Test
    public void test() {
        
        NewInterface.staticMethod();
        new SimpleImpl().defaultMethod();
    }
}
        
而且 default 方法的调用有一点需要特别注意。我们知道在 Java 中是单继承的,但是是可以实现多个接口的,所以,当一个类实现了多个接口之后,如果多个接口有着相同的 default 方法,即方法名和参数列表相同。那么此时就会出现问题,无法识别到底是调用的哪个接口的方法,这个时候就必须要在实现类里面显式重写 default 的方法,而关于 default 的方法的重写,我们在实现类中不需要继续出现 default 关键字也不能出现 default 关键字。
        

public class SimpleImpl implements NewInterface {
 
    @Override
    public void getInfo() {
        
        // TODO Auto-generated method stub
        System.out.println("INFO");
        defaultMethod();
    }
 
    public void defaultMethod() {
        
        System.out.println("Impl default Method");
    }
}

重写的 default 方法必须的访问权限必须是 public,因为 default 方法除了没有显式的访问修饰符外,只能用 public 访问限定符来修饰,而我们知道在 Java 中,要重写一个方法,访问限定符一定要大于或等于父类或者接口指定的访问限定符范围,而且方法声明处抛出的异常也要大于后者。所以访问权限必须是 public。

最后,当 default 方法和实现类继承的父类的方法同名时,优先调用父类的方法

10. 内部类

在 Java 中,允许一个类定义位于另一个类的内部,前者称为内部类,后者称为外部类。
    
Inner class 的名字不能与包含它的外部类类名相同。

10.1 成员内部类(static 成员内部类和非 static 成员内部类)

class Outer {
    
    private int s;
    
    public class Inner {
    
        public void mb() {
            
        	s = 100;
        	System.out.println("在内部类Inner中s=" + s);
    	}
   }
    
    public void ma() {
        
        Inner i = new Inner();
        i.mb();
    } 
}

public class InnerTest {
    
    public static void main(String args[]) {
       
        Outer o = new Outer();
        o.ma();
	}
}

10.2 局部内部类(不谈修饰符)

class 外部类 {
    方法() {
        class 局部内部类 {}
    }
    
    {
        class 局部内部类 {}
    }
}


1、只能在声明它的方法或代码块中使用,而且是先声明后使用,除此之外的任何地方都不能使用该类。

2、但是它的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类的父类或父接口类型。
    
3、局部内部类可以使用外部方法的局部变量,但是必须是 final 的。

10.3 匿名内部类

匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。
    
一个匿名内部类一定是在 new 后面,用其隐含实现一个接口或实现一个类。
    
匿名内部类的特点:
    1、匿名内部类必须继承父类或实现接口;
    2、匿名内部类只能有一个对象;
    3、匿名内部类对象只能使用多态形式引用。
 
        
interface A {
        
    public abstract void fun1();
}

public class Outer {
    
    public static void main(String[] args) {
        
        new Outer().callInner(new A() {
            //接口是不能 new 但此处比较特殊是子类对象实现接口,只不过没有为对象取名
            public void fun1() {
                
                System.out.println("implement for fun1");
            }
        });// 两步写成一步了
    }

    public void callInner(A a) {
        
        a.fun1();
    }
}

11. 异常处理

异常:
    在 Java 语言中,将程序执行中发生"不正常情况称为" "异常"。
    
(开发过程中的语法错误和逻辑错误不是异常)
    
Java 程序在执行过程中所发生的异常事件可分为两类:
    1、Error:
    	Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
    
    2、Exception:其他因为编程错误或偶然的外在因素导致的一般性问题,可以使用针对性代码进行处理。

11.1 异常处理机制一:try-catch-finally

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行 x/y 运算时,要检测分母为0,数据为空,输入的不是数据而是字符串等。过多的 if-else 分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。
    
    a.Java 程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给 Java 运行时系统,这个过程称为抛出(throw)异常。
    
    b.如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。
    
    c.程序员通常只能处理 Exception,而对 Error 无能为力。
    
异常处理是通过 try-catch-finally 语句实现的。
    
try{
    //可能产生异常的代码
} catch (ExceptionName1 e) {
    //当产生ExceptionName1型异常时的处置措施
} catch (ExceptionName2 e) {
    //当产生ExceptionName2型异常时的处置措施
}
[finally {
    //无论是否发生异常,都无条件执行的语句
}]

如果抛出的异常是 IOException 等类型的非运行时异常,则必须捕获,否则编译错误。也就是说,我们必须处理"编译时异常",将异常进行捕捉,转化为"运行时异常"

11.2 异常处理机制二:throws

a.如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而"由该方法的调用者负责处理"。
    
b.在方法声明中用 throws 语句可以声明抛出异常的列表,throws 后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。
    
c.重写方法不能抛出比被重写方法范围更大的异常。

12. 其他关键字

12.1 class

class 关键字用来声明新的 Java 类,该类是相关变量和 / 或方法的集合。

类是面向对象的程序设计方法的基本构造单位。

类通常代表某种实际实体,如几何形状或人。

类是对象的模版。

每个对象都是类的一个实例。

要使用类,通常使用 new 操作符将类的对象实例化,然后调用类的方法来访问类的功能。

12.2 new

new 关键字用于创建类的新实例。 

new 关键字后面的参数必须是类名,并且类名的后面必须是一组构造方法参数(必须带括号)。 

参数集合必须与类的构造方法的签名匹配。 

= 左侧的变量的类型必须与要实例化的类或接口具有赋值兼容关系。

12.3 instanceof

instanceof 运算符的前一个操作符是一个引用变量,后一个操作数通常是一个类(可以是接口),用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回 true,否则返回 false