Java基础:面向对象的开发

背景:

        Java之所以能风靡全球并且一直保持领先的原因之一:必定是他面向对象编程(OOP)的思想。诸如更快的开发和更廉价的维护成本,使建模变得更简单,能够很轻松应对大体量的工程项目,使得代码更加优雅等优势已经让人烂熟于心。
        当然,对于初学者而言,面向对象是肯定没有面向过程那样容易理解的。首先,我们要明白,什么是对象?

类和对象

        所谓对象,可谓是万物皆对象。比如一台电脑,某部手机等。要了解对象,就要先了解类。
        类:类是一类事物的抽象,这类事物拥有相同的特征。在Java中,我们可以理解为拥有相同属性和方法的一类事物的抽象描述。举个例子。人可以抽象为一个类(Person),如果需要,我们可以继续将人这个类细化为 男人(Man)和 (Woman)两个子类。当然我们还能以其他维度去抽象出其他子类。而作为类,他的本质作用是定义这类事物所拥有的属性和方法,并不一定要给出具体的属性值和方法行为。 它仅仅只是做一个定义或者规范。
        对象:我们在Java中经常说:实例化一个对象,或者类的实例化。其实本质就是从抽象的类中,去构造出一个具体的对象。比如我们可以实例化一个叫做张三,跑步很快的(男)人。这个过程叫做实例化,你可以理解为具体化的过程,相当于我们从(男)人这个类中去实例化一个具体的人张三,赋予他一个名字(属性)叫做张三,赋予它一个行为(方法)叫做跑步,并且跑步跑得很快。

三大特征

        面向对象的三大特征:封装、继承、多态

        封装:

        在Java中,封装其实指的是将属性和实现功能的细节封装成抽象的类,仅仅提供给外界访问接口和功能说明。比如查询订单的接口:你会得到接口地址和这个接口的功能,而并不知道这个接口做了哪些逻辑。实际上,作为调用者,也确实不需要知道接口的实际实现功能的过程。并且,封装会把属性私有化(对每个属性提供getter和setter方法),以防外界非法修改。由此可见,封装的好处就是增强安全性和简化编程。
        在对属性私有化一般使用private关键字,以下是各个关键字的访问范围,可以给我们提供不同级别的保护。

访问修饰符同类同包子类其他包
public
protected
default
private
        继承:

        继承就是子类继承父类的行为,使得子类获得父类的属性和行为。子类也可以根据自身实际情况去重写父类属性和方法,或者拓展新的属性和方法。不过要注意的是:Java不支持多继承(但支持多级继承)。继承使用的关键字是extends。
        在开发过程中,我们可以把公共的方法抽取到父类中去。有效的抽取能提高代码的复用率和代码维护性。继承的弊端是会增加代码耦合度(这边的耦合是相对的,有效的继承是利大于弊)。

         继承的注意点:
         1. 子类只能继承(重写)父类所有非私有的属性和方法(父类内部使用的属性依然可以封装起来)。
         2. 继承无法继承构造方法,只能通过super访问父类构造方法。
         3. 子类重写父类方法时,访问权限不能更低。
         4. 父类的静态方法,子类也必须通过静态方法去进行“重写”(子类为静态,父类也必须为静态)。
         5. 子类构所有构造方法默认都会访问父类的无参构造方法(子类每一个构造方法的第一条语句默认都是:super().只是没有显现出来),所以在设计父类的时候,最好保证父类有一个无参构造。
         6. 子父类初始化顺序,先初始化父类,再初始化子类(分层初始化)。
         7. 因为子类会调用父类的数据,所以默认会初始化父类。


/**
* 这边引入的一个知识点:
* 先是加载类,
* 加载类的时候,
*     会先初始化成员变量和成员方法,
*     然后实例化类,会调用构造方法,
*     所以就导致一个现象,
*         先初始化成员变量,再加载构造。
* 
* 一个类的初始化过程:
*     成员变量的初始化过程
*         默认初始化
*         显示初始化
*         构造方法初始化
* 子父类的初始化(分层初始化)
*     先进行父类初始化,然后进行子类初始化。
*/    

class Y{
	public Y() {
		System.out.print("Y ");
	}
}

class X {
	Y b = new Y();
	public X() {
		System.out.print("X ");
	}
}

public class Z extends X{
	Y y = new Y();
	public Z() {
		super();
		System.out.print("Z ");
	}
	public static void main(String[] args){
		new Z();
	}
}


// 结果YXYZ
/**
 * 解析:
 * 在main函数中,new Z();的时候,因为Z继承了X,所以先初始化父类X。
 * 而父类中,有成员变量和构造方法。而加载类的时候,又会先初始化成员变量和成员方法,再调用构造方法实例化类。
 * 所以:
 * 先执行了X 中的 Y y = new Y(); 打印了Y 
 * 然后执行X 中的构造方法实例化类,打印了X
 * 执行完上面步骤,初始化父类完成,继而执行Z中的 Y y = new Y(); 打印了Y
 * 最后,执行Z中的构造方法实例化Z,打印了Z
 */
/** 比较常见的继承父类方法,并拓展父类方法。
 * 这边用到super调用父类被覆盖的方法。
 */
class Bird{
	public void sing(){
		System.out.println("Bird sing~");
	}
}

class Parrot extends Bird{
	public void sing(){
		super.sing();
		System.out.println("Parrot also can repeat words!");
	}
}

public class BirdSing {
	public static void main(String[] args) {
		Parrot parrot = new Parrot();
		parrot.sing();
	}
}
/*
	看程序写结果:
		A:一个类的静态代码块,构造代码块,构造方法的执行流程
			静态代码块 > 构造代码块 > 构造方法
		B:静态的内容是随着类的加载而加载
			静态代码块的内容会优先执行
		C:子类初始化之前先会进行父类的初始化
		
	结果是:
		静态代码块Fu
		静态代码块Zi
		构造代码块Fu
		构造方法Fu
		构造代码块Zi
		构造方法Zi
	原因:
		其实Zi z = new Zi();过程是: 先Zi z。先把Zi类加载到栈内存中(加载类),然后再去执行  = new Zi(),实例化Zi,在堆内存中给zi开辟空间。
		在Zi z 时(加载类):要先加载Zi类,在加载子类的时候发现继承了Fu类,所以在加载父类的时候,先执行了Fu()的静态代码块
然后执行子类代码块。
		在= new Zi();时(构造):发现有父类,所以先走父类的构造代码块,父类的构造方法,再走子类的构造代码块,子类的构造方法。 
	
*/
class Fu {
	static {
		System.out.println("静态代码块Fu");
	}

	{
		System.out.println("构造代码块Fu");
	}

	public Fu() {
		System.out.println("构造方法Fu");
	}
}

class Zi extends Fu {
	static {
		System.out.println("静态代码块Zi");
	}

	{
		System.out.println("构造代码块Zi");
	}

	public Zi() {
		System.out.println("构造方法Zi");
	}
}

class Test2 {
	public static void main(String[] args) {
		Zi z = new Zi();
	}
}

        继承概念的实现方式有三类:实现继承、接口继承和可视继承。
         1. 实现继承:直接使用父类的属性和方法,无需额外编码。
         2. 接口继承:继承父类属性和方法的名称,子类需要提供实现能力。
         3. 可视继承:子窗体(类)使用基窗体(类)的外观和实现代码的能力。

        多态:

         多态可以让同一个行为具有多个不同表现形式或形态的能力。比如打印机,最终可以有激光打印机和喷墨打印机,黑白打印机和彩色打印机等。我们常见的多态有List<Integer> list = new ArrayList<>();
         多态是一种动态绑定(dynamic binding:指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法),他可以有效的解决继承带来的耦合问题。
         多态实现必备条件:

         1. 要有继承关系或实现关系
         2. 要有方法重写
         3. 要有父类引用指向子类对象: 父类 f = new 子类();

         多态中的成员访问特点:

         1. 成员变量
             编译看等号左边(父类),运行看左边(父类)。
         2. 构造方法
            创建子类对象时,访问父类的构造方法,对父类的数据进行初始化
         2. 成员方法
            编译看等号左边(父类),运行看右边(子类,其实父类引用还是会调用自己的方法,然后因为是重写导致出现了子类的结果,并非父类调用到子类的方法)(出现重写(重写就是覆盖)) 父类不能使用子类特有的功能(Fu f=new zi() f是父类的对象)。
         1. 静态方法
            编译看、等号左边(父类),运行看左边(父类)。 静态和类相关,算不上重写,所以访问还是看左边。
            静态的方法可以被继承,但是不能重写。如果父类中有一个静态的方法,子类也有一个与其方法名,参数类型,参数个数都一样的方法,并且也有static关键字修饰,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。通俗的讲就是父类的方法和子类的方法是两个没有关系的方法,具体调用哪一个方法是看是哪个对象的引用;这种父子类方法也不在存在多态的性质。《Java编程思想》中这样提到“只有普通的方法调用可以是多态的”。

        多态的好处:

        1. 提高了代码的维护性(继承保证)
        2. 提高代码的扩展性

        多态的弊端:
        1. 多态不能使用子类特有的功能。如需使用子类特有功能,需要向下转型。

        在Java中,我们提供了解决多态弊端的方法:向下转型。
        向上转型和向下转型是多态中常用的一些方式。向上转型:Fu f = new Zi(); 而向下转型:Zi z = (Zi)f; 向下转型,只能转自己或者自己的“父集”。

/*
	多态的弊端:
		不能使用子类的特有功能(子类有得方法,父类没有的话,可以编译通过,但是运行的时候,用父类的引用调用子类的方法时调用不到的。)。
		
	我就想使用子类的特有功能?行不行?
		行。
		
	怎么用呢?
		A:创建子类对象调用方法即可。(可以,但是很多时候不合理。而且,太占内存了)
		B:把父类的引用强制转换为子类的引用。(向下转型)
		
	对象间的转型问题:
		向上转型:
			Fu f = new Zi();
		向下转型:
			Zi z = (Zi)f; //要求该f必须是能够转换为Zi的。
*/
class Fu {
	public void show() {
		System.out.println("show fu");
	}
}

class Zi extends Fu {
	public void show() {
		System.out.println("show zi");
	}
	
	public void method() {
		System.out.println("method zi");
	}

}

class DuoTaiDemo4 {
	public static void main(String[] args) {
		// 向上转型
		Fu f = new Zi();
		f.show();
		//f.method();
		
		//创建子类对象
		//Zi z = new Zi();
		//z.show();
		//z.method();
		
		//你能够把子的对象赋值给父亲,那么我能不能把父的引用赋值给子的引用呢?
		//如果可以,但是如下
		Zi z = (Zi)f;
		z.show();
		z.method();
	}
}

五大基本原则

  • 单一职责原则(Single Responsibility Principle)
    单一职责原则的规则是:每一个类应该专注于做一件事情。可以降低类的复杂性和变更引起的风险,提高类的可读性和维护性。各司其职,有条不紊。此原则的核心就是解耦和增强内聚性。
  • 开放封闭原则(Open Close Principle)
    开闭原则的规则是:面向扩展开放,面向修改关闭。在设计一个模块时,应该使得这个模块不被修改的前提下被拓展。多态就是一种开放封闭原则的践行方式。
  • 里氏替换原则(Liskov Substitution Principle)
    里氏替换原则的规则是:超类存在的地方,子类是可以替换的。里氏替换原则的核心是:子类可以扩展父类的功能,但不能改变父类原有的功能。 里氏替换原则这样做,能够保证,核心公共功能都由父类保证,在需要修改这些功能时,只需要修改父类方法就行。如果存在子类覆盖,那么这些子类就不能及时获取到父类的修改。尤其是在项目比较庞大的时候。往往会忽略这些子类的存在,导致一些异常问题。
    里氏替换原则强调的是不修改父类原有功能,主要是防止覆盖父类方法后,让父类失去“统一管理”的功能,在修改父类后,子类无法获取最新修改。这似乎和多态有点矛盾。这该怎么办呢?
    其实我们可以这么理解,里氏替换原则其实是一种指导。在公共配置中,我们常会在父类里面写好实现方法,子类继承即可,这样可以避免代码重复性。这种情况下,尽量遵守里氏替换原则,因为公共方法需要“统筹安排”,而类似业务问题,本来就是存在各不相同的表现或者实现方式,这种情况下,一般的做法是,父类中仅定义抽象方法,子类去做特有实现,当然,这种情况下也可能会有小部分业务抽取,这种时候,我们需要拥抱变化,要注意尽量规避日后的重写。
  • 依赖倒置原则(Dependence Inversion Principle)
    里氏替换原则强调在设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。比如抽象不应该依赖细节,而细节应该依赖抽象。这样可以有效地降低耦合。增强代码可读性和可维护性。
    这么说还是比较抽象,我们用代码来讲解。我们以前比较喜欢这么写代码:
public class Course {
	public void javaCourse(){
		System.out.println("Java Study");
	}
	public void pythonCourse(){
		System.out.println("Python Study");
	}
	// ...
}

public class StudyDemo {
	public static void main(String[] args) {
		Course course = new Course();
		course.javaCourse();
		course.pythonCourse();
	}
}

当新增课程时,就会去Course类里面新增方法,所以,每次新增课程,都需要从底层实现到高层调用依次地修改代码。这就意味着高层调用依赖着底层的实现。这样的新增,看上去不会动到原有代码,似乎是安全的。但是实际情况要比这个复杂很多,可能某次一不小心就没有hold住变化。
所以,最好的打算是,我们写好的代码,尽量不要去修改它。
我们仔细思考,课程是一个平行概念。这样我们就可以抽取一个接口,然后每次新增课程,实现它就行。

// 接口
public interface ICourse {
	void study();
}

// 课程方法实现
public class JavaCourse implements ICourse{
	@Override
	public void study() {
		System.out.println("Java Study!");
	}
}

// 课程方法实现
public class PythonCourse implements ICourse{
	@Override
	public void study() {
		System.out.println("Python Course");
	}
}
// 提供调用工厂
public class StudyFactory {
	public void study(ICourse course) {
		course.study();
	}
}

// 业务高层
public class StudyDemo {
	public static void main(String[] args) {
		StudyFactory factory = new StudyFactory();
		factory.study(new JavaCourse());
		factory.study(new PythonCourse());
	}
}
  • 接口隔离原则(Interface Segregation Principle)
    接口隔离原则的规则是:客户端不应该依赖他们不需要的方法,依赖的接口应以最小为原则。
    这就要求我们定义接口,也要以最小为原则,应将方法分类分组,不应该将毫无关系的方法塞进一个接口。所以我们可以将接口隔离理解为:我们可以利用接口定义来最小化分组功能方法。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值