面向对象的三大特点

面向对象有三大特点:封装性、继承性和多态性,它们是面向对象程序设计的灵魂所在,下面一一给予简介。

封装的含义

封装 (Encapsulation)是将描述某类事物的数据与处理这些数据的函数封装在一起,形成一个有机整体,称为类。类所具有的封装性可使程序模块具有良好的独立性与可维护性,这对大型程序的开发是特别重要的。类中的私有数据在类的外部不能直接使用,外部只能通过类的公共接口方法(函数)来处理类中的数据,从而使数据的安全性得到保证。封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而仅需要通过外部接口,特定的访问权限来使用类的成员。

一旦设计好类,就可以实例化该类的对象。我们在形成一个对象的同时也界定了对象与外界的内外隔离。至于对象的属性、行为等实现的细节则被封装在对象的内部。外部的使用者和其他的对象只能经由原先规划好的接口和对象交互。

我们可用一个鸡蛋的三重构造来比拟一个对象,如下图所示。

属性(Attributes)好比蛋黄,它隐藏于中心,不能直接接触,它代表的对象的状态(State)。

行为(Behaviors)好比蛋白,它可以经由接口与外界交互而改变内部的属性值,并把这种改变通过接口呈现出来。

接口(Interface)好比蛋壳,它可以与外界直接接触。外部也只能通过公开的接口方法来改变对象内部的属性(数据)值,从而使类中数据的安全性得到保证。

继承的含义

对象(Object)是类(Class)的一个实例(Instance)。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。继承性是面向对象的第二大特征。继承(Inheritance)是面向对象程序设计中软件复用的关键技术,通过继承,可以进一步扩充新的特性,适应新的需求。这种可复用、可扩充技术在很大程度上降低了大型软件的开发难度,从而提高软件的开发效率。

中国古代逻辑学家公孙龙(约公元前320–250年)提出的一个著名的逻辑问题:“白马非马”。在《公孙龙子·白马论》有这样的描述:“白马非马,可乎?”曰:“可。”曰:“何哉?”曰:“马者,所以命形也。白者,所以命色也。命色者,非命形也,故曰白马非马。”

在一定程度上,公孙龙的“白马非马”逻辑体现了面向对象“继承”思想。公孙龙声称:“白马不是马”,其论证过程如下。

“马”只有一个特征。马的特征。

而“白马”有两个特征。⑴ 马的特征;⑵ 白色的。

因此,在逻辑上,拥有2个特征的“白马”不等同于只有1个特征的“马”,所以“白马非马”。而从集合论上来考虑,马与白马是两个不同的集合,但是“马”这个集合包含了另一个集合“白马”, 后者是前者的真子集,集合不等同于它的真子集。

当我们说某一个新类A继承某一既有类B时,表示这个新类A具有既有类B的所有成员,同时对既有类的成员做出修改,或是增加了新的成员。保持已有类的特性而构造新类的过程称为继承。在已有类的基础上新增自己的特性而产生新类的过程称为派生。我们把既有类称为基类(base class)、超类(super class)或者父类(parentclass),而派生出的新类,称为派生类(derived class)或子类(subclass)。

继承可以使得子类自动具有父类的各种属性和方法,而不需要再次编写相同的代码,从而达到类的复用目的。这样,子类A可以对父类B的定义加以扩充,从而制定出一个不同于父类的定义,让子类具备新的特性。

针对公孙龙的“白马非马”的逻辑,从面向对象上角度来考虑,“马”与“白马”是两个不同的类,“马”是父类,而“白马”则是“马”的子类(或者称为派生类),后者继承了前者有关“马”的特性,同时添加了自己的新特性——“白色”,故此,父类也不等于它的子类。

继承的目的在于实现代码重用,对已有的成熟的功能,子类从父类执行“拿来主义”。而派生的目的则在于,当新的问题出现时,原有代码无法解决(或不能完全解决)时,需要对原有代码进行全部(或部分)改造。对于Java程序而言,设计孤立的类是比较容易的,难的是正确设计好的类层次结构,以达到代码高效重用的目的。

多态的含义

多态(Polymorphisn),从字面上理解,多态就是一种类型表现出多种状态。这也是人类思维方式的一种直接模拟,可以利用多态的特征,用统一的标识来完成这些功能。在Java中,多态性分为两类。

1. 方法多态性,体现在方法的重载与覆写上。

方法的重载是指同一个方法名称,根据其传入的参数类型、个数和顺序的不同,所调用的方法体也不同,即同一个方法名称在一个类中有不同的功能实现。

方法的覆写是指父类之中的一个方法名称,在不同的子类有不同的功能实现,而后依据实例化子类的不同,同一个方法,可以完成不同的功能。

2. 对象多态性,体现在父、子对象之间的转型上。

这个层面上,多态性是允许将父对象设置成为与一个或更多的子对象相等的技术,通过赋值之后,父对象就可以根据当前赋值给的不同子对象,以子对象的特性加以运作。多态意味着相同的(父类)信息发送给不同的(子)对象,每个子对象表现出不同的形态。

多态中的一个核心概念就是,子类(派生类)对象可以视为父类(基类)对象。这是容易理解的,如下图所示的继承关系中,鱼(Fish)类、鸟(Bird)类和马(Horse)类都继承于父类Animal(动物),对于这些实例化对象,我们可以说,鱼(子类对象)是动物(父类对象);鸟(子类对象)是动物(父类对象);同样的,马(子类对象)是动物(父类对象)。

在Java编程里,我们可以用下图来描述。

在上述代码中,第1~4行,分别实例化父类对象a以及子类对象f、b和h。由于Fish类、Bird类和Horse类均继承于父类Animal,所以子类均继承了父类的move()方法。由于父类Animal的move()过于抽象,不能反映Fish、Bird和Horse等子类中“个性化”的move()方法。这样,势必需要在Fish、Bird和Horse等子类中重新定义move()方法,这样就“覆写”了父类的同名方法。在第2~4行完成定义后,我们自然可以做到。

这并不是多态的表现,因为三种不同的对象,对应了三种不同的移动方式,“三对三”平均下来就是“一对一”,何“多”之有呢?当子对象很多时,这种描述方式非常地繁琐。

我们希望达到如上述代码第5~7行所示的效果,统一用父类对象a来接收子类对象f、r和h,然后用统一的接口“a.move()”,展现出不同的形态:当“a = f”时,“a.move()”表现出的是子类Fish的move()方法——鱼儿游,而非父类的move()方法。类似的,当“a = b”时,“a.move()”表现出的是子类Bird的move()方法——鸟儿飞,而非父类的move()方法。当“a = h”时,“a.move()”表现出的是子类Horse的move()方法——马儿跑,而非父类的move()方法。这样,就达到了“一对多”的效果——多态就在这里。

父、子对象之间的转型包括如下两种形式。

⑴ 向上转型(Upcast)(自动转型):父类 父类对象 = 子类实例。

将子类对象赋值给父类对象,这样将子类对象自动转换为父类对象。这种转换方式是安全的。例如,我们可以说鱼是动物,鸟是动物,马是动物。这种向上转型在多态中应用得很广泛。

⑵ 向下转型(Downcast)(强制转型):子类 子类对象 = (子类) 父类对象。

将父类对象赋值给子类对象。这种转换方式是非安全的。例如,如果我们说动物是鱼,动物是鸟,动物是马,这类描述是不全面的。因此,在特定背景下如果需要父类对象转换为子类对象,就必须使用强制类型转换。这种向下转型用的比较少。

封装的实现

Java访问权限修饰符

在Java中有四种访问权限:公有(public)、私有(private)、保护(protected)、默认(default)。但访问权限修饰符只有三种,因为默认访问权限没有访问权限修饰符。默认访问权限是包访问权限,即在没有任何修饰符的情况下定义的类,属性和方法在一个包内都是可访问的。具体访问权限的规定如下表所示。

封装问题引例

前面我们给出类封装性的本质,但是对初学者来说,这个概念可能还是比较抽象。从哲学的角度来说,我们要“透过现象看本质”,现在本质给出了,如果还不能理解的话,其实是我们没有落实“透过现象”这个流程。下面我们给出一个实例(现象)来说明上面论述的本质。

假设我们把对象的属性(数据)暴露出来,外界可以任意接触到它甚至能改变它。大家可以先看下面的程序,看看会产生什么问题。

类的封装性使用引例——一只品质不可控的猫(TestCat.Java)


首先我们来分析一下MyCat类。第15行,通过public修饰符,开放MyCat的属性(weight)给外界,这意味着外界可以通过“对象名.属性名”的方式来访问(读或写)这个属性。第16行声明一个无参构造方法,在本例中无明显含义。

第05行,定义一个对象aCat。第07行通过点操作符获得这个对象的值。第08行输出这个对象的属性值。我们需要重点关注第06行,它通过“点操作符”设置这个对象的值(-10.0)f。一般意义上,“-10.0f”是一个普通的合法的单精度浮点数,因此在纯语法上,它给weight赋值没有任何问题。但是对于一个真正的对象(猫)来说,这是完全不能接受的,一个猫的重量(weight)怎么可能为负值?这明显是“一只不合格的猫”,但是由于weight这个属性开放给外界,“猫的体重值”无法做到“独立自主”,因为它的值可被任何外界的行为所影响。

这就好比要加工一件产品一样,加工的原料本身就有问题,那么最终加工出来的产品也一定是一个不合格的产品。而导致这种错误的原因,就是因为程序在原料的入口处没有检验,而加工的原料原本就是不合格的,那么加工出来的产品也必然是一个不合要求的产品。那么如何来改善这种状况呢?这时,类的封装就可以起到很好的作用。请参看下面的案例。

类的封装实例

大家可以发现,之前所列举的程序都是用对象直接访问类中的属性,这在面向对象法则中是不允许的。所以为了避免程序中这种错误的发生,在一般的开发中往往要将类中的属性封装(private)。对范例TestCat.Java做了相应的修改后,就可构成下面的程序TestCat2.Java。

类的封装实例——一只难以访问的猫。

第12~16行声明了一个新的类MyCat,类中有属性weight,与前面的范例不同的是,这里的属性在声明时前面加上了private修饰符。

可以看到,本程序与上面的范例相比,在声明属性weight前,多了个修饰符private(私有的)。而就是这一个小小的关键字,却使得下面同样的代码连编译都无法通过。

其所提示的错误为如下图所示。

这里的“字段(Field)”就是Java里“数据属性”的含义。这是因为weight为私有的,所以外界不能由对象直接进行访问这些私有属性,因此代码第06-07行是无法通过编译的。

这样,虽然可以通过封装,达到外界无法访问私有属性的目的。但是如果非要给对象的属性赋值的话,这一矛盾该如何解决呢?程序设计人员一般在类的设计时,都会设计存或取这些属性的公共接口,这些接口的外在表现形式都是公有(public)方法。而在这些方法里,我们可以对存或取属性的操作,实施合理的检查,以达到保护属性数据的目的。通常,对属性值设置的方法被命名为SetXxx(),其中Xxx为任意有意义的名称,这类方法可统称为Setter方法.而对取属性值的方法通常被命名为GetYyy,其中Yyy为任意有意义的名称,这类方法可统称为Getter方法。请看下面的范例。

类私有属性的Setter和Getter方法——一只品质可控的猫(TestCat3.java)。



第17~32行加入了SetWeight( float wt)和float GetWeight()等公有方法,外界可以通过这些接口来设置和取得类中的私有属性weight。

第06行调用了SetWeight()方法,同时传进一个-10的不合理体重。

第17~28行,在完成设置体重时,程序中加了些判断语句,如果传入的数值大于0,则将值赋给weight属性,否则给出警告信息,并采用默认值。通过这个方法可以看出,经由公有接口来对属性值实施操作,我们可以在这些接口里对这些值实施“管控”,从而能更好地控制属性成员。

可以看到在本程序中,由于weight传进了一个-10的不合理的数值(-10后面的f表示这个数是float类型),这样在设置MyCat中属性时,因不满足条件而不能被设置成功,所以weight的值采用自己的默认值— 10。这样在输出的时候可以看到,那些错误的数据并没有被赋到属性上去,而只输出了默认值。

由此可以知道,用private可以将属性封装起来,当然用private也可以封装方法,封装的形式如下。


提示
用private声明的属性或方法只能在其类的内部被调用,而不能在类的外部被调用。大家可以先暂时简单地理解为在类外部不能用对象去调用private声明的属性或方法。

下面的这个范例添加了一个MakeSound()方法,通过修饰符private(私有)将其封装了起来。

方法的封装使用(TestCat4.Java)



第35行将MakeSound()方法用private来声明。第10行,想通过对象的点操作符“.”来尝试调用这个私有方法。由于私有方法是不对外公开的,因此得到上述的编译错误:“类型 MyCat 中的方法MakeSound()不可视”。

一旦方法的访问权限被声明为private(私有的),那么这个方法就只能在类的内部被访问了。如果想让上述代码编译成功,其中一种方法是将10行的代码删除,而在GetWeight()添加调用MakeSound()方法的语句,如下所示。

访问权限控制符是对类外而言的,而在同一类中,所有的类成员属性及方法都是相互可见的,也就是说,它们之间是可以相互访问的。程序成功运行的结果如下图所示。

如果类中的某些数据在初始化后不想再被外界修改,则可以使用构造方法配合私有化的Setter 函数来实现该数据的封装,如下例所示。

使用构造函数实现数据的封装(TestEncapsulation.Java)。




在第07~11行中的MyCat类的构造方法,通过调用私有化SetHeight()方法(在23~30定义)和私有化SetWeight()方法(在14~21定义)来对height和weight进行初始化。

这样类MyCat的对象aCat一经实例化(41行),name和age私有属性便不能再进行修改,因为构造方法只能在实例化对象时自动调用一次,而SetHeight()方法和SetWeight()方法的访问权限为私有类型,外界不能调用,这样就实现了封装的目的。

通过构造函数进行初始化类中的私有属性能够达到一定的封装效果,但是也不能过度相信这种封装,有些情况下即使这样做,私有属性也有可能被外界修改。

提示
大家可能会问,到底什么时候需要封装,什么时候不用封装。在这里可以告诉读者,关于封装与否并没有一个明确的规定,不过从程序设计的角度来说,一般说来设计较好的程序的类中的属性都是需要封装的。此时要设置或取得属性值,则只能使用Setter和Getter方法,这是一个比较标准的做法。

封装问题的总结

上面的几个例子简单的介绍了如何使用Java中的封装技术。下面对Java中封装的知识进行总结。

在Java中,最基本的封装单元是类,类是基于面向对象思想编程语言的基础,程序员可以把具有相同业务性质的代码封装在一个类里,通过接口方法向外部代码提供服务,同时向外部代码屏蔽类里服务的具体实现方式。

数据封装的最重要的目的是在于要实现“信息隐藏(Information Hidding)”。在类中的“数据成员(属性)”或者“方法成员”,可以使用关键字“public”、”private”、”protected”来设置各成员的访问权限。例如,我们可以把“SetHeight()”这个方法封装在“MyCat”类中,用private设置不开放给外界使用,则“TestEncapsulation类”就无法调用“SetHeight()”这个方法来设置Mycat中的height属性值。

封装性是面向对象程序设计的原则之一。它规定对象应对外部环境隐藏它们的内部工作方式。良好的封装可以提高代码的模块化程度,它防止了对象之间不良的相互影响。使程序达到强内聚(许多功能尽量在类的内部独立完成,不让外面干预),弱耦合(提供给外部尽量少的方法调用)的最终目标。

实现封装应该注意的问题

实现良好的封装是面向对象程序员的目标,而在实现封装时应该注意的问题也很多,在这里仅提出一个比较突出的问题希望能对读者有所启发。

实现封装是对外部而言的,我们总是要有选择地提供一些类似于setXxx()或者getYyy()的公有接口,以“可控”的方式来设置或读取类内部的属性值。有些属性值在初始化以后就不允许再进行修改,对这样的属性不设置setXxx()方法是明智的,或者这类设置Setter方法的访问权限设置为私有的,然后直接通过构造方法来调用这些方法,从而实现一次性的初始化。在很多情况下,这样似乎万无一失了。

一旦实例化对象就无法再改变name和age的值,但是如果要隐藏的数据是一个对象会怎样?请参见下面的范例。

返回引用数据时应该注意的问题(ReturnVariable.Java)



在02~18行创建了一个TestReturn类,为了封装类中的一个ArrayList类型(一种长度可变数组,将在第20章详细介绍)的属性intArray,将其设置为private,为了进一步实现封装在07~12行的构造函数中对其进行初始化,并且没有提供相应的设置Setter方法。为了得到该属性在14~17行定义了一个getIntArray()方法。

第23行实例化了TestReturn类,定义了testReturn对象,在构造这个对象的同时,自动调用构造方法,即也初始化inArray的属性值。

第25行通过getIntArray()函数得到intArray私有属性,并在26行输出这个数组的大小。

第28行向得到的intArray中添加了一个int类型数据成员4,

第30行重新获得intArray的值,第33行输出时它的长度已经发生变化,其数组的长度由原来的3变成了4。

在本例中需要注意的是当通过getIntArray()方法返回私有变量时,如果返回的是对该私有变量对象的引用,而不是副本。引用的含义在于,通过它直接能找到所操作对象在内存中的原始位置,则在该类的外部对其进行的修改会影响到内部。这类问题的解决办法是,如果返回值是对数据的引用则显式创建该数据的副本,然后返回该副本即可。

在类似上一个范例中(TestEncapsulation.Java)就不会出现这种情况,因为类的私有变量是float类型,它们都属于基本数据类型,通过方法返回的是其副本(即是以传值的方法传递的,外部接收的实参(如ht、wt等)和方法内部的参数(如height、weight等),它们压根就在不同的内存空间,在通过拷贝完成值传递后,它们就没有任何联系,所以在外部值(如ht、wt等)的修改不会影响到内部参数(如height、weight等)。

继承的实现

继承的基本概念

在Java中,通过继承可以简化类的定义,扩展类的功能。在Java中支持类的单继承和多层继承,但是不支持多继承,即一个类只能继承一个类而不能继承多个类。

实现继承的格式如下。

extends 是Java中的关键词。Java继承只能直接继承父类中的公有属性和公有方法,而隐含的(不可见的)继承了私有属性。

现在假设有一个Person类,里面有name与age两个属性,而另外一个Student类,需要有name、age、school等3个属性,如下图所示。由于Person中已存在有name和age两个属性,所以不希望在Student类中再次重新声明这两个属性,这时就需考虑是否可将Person类中的内容继续保留到Student类中,这就引出了接下来要介绍的类的继承概念。


在这里希望Student类能够将 Person类的内容继承下来后继续使用,可用下图表示,这样就可以达到代码复用的目的。

Java类的继承可用下面的语法来表示。

继承问题的引出

我们首先观察一下下面这个例子,下面的例子中包括需要Person和Student两个类。

继承的引出(LeadInherit.Java)



上面代码的功能很简单,在01~12行定义了Person类,其中04~07行为Person类的构造方法。14~29行定义了Student类,并分别定义了其属性和方法。34行和37行分别实例Person类和Student类。

通过具体的代码编写之后,我们发现这两个类中有很多的相同部分,例如两个类中都有name、age属性和speak()方法。这就造成了代码的臃肿。软件开发的目标是“软件复用,尽量没有重复”,因此有必要对上述范例实施改造。

实现继承

为了简化上述范例,我们使用继承来完成相同的功能,请参见下面的范例。

类的继承演示程序(InheritDemo.Java)



第01~11行声明了一个名为Person的类,里面有name和age两个属性和一个方法speak()。其中,第04~07行定义了Person类的构造方法Person( ),用于初始化name和age两个属性。为了区分构造方法Person( )中同名的形参和类中属性名,赋值运算符“=”左侧的“this.”表明左侧的name和age是来自类中。

第13~22行声明了一个名为Student的类,并继承自Person类(使用了extends关键字)。在Student类中,定义了school属性和study()方法。其中,第15~18行定义了Student类的构造方法Student()。虽然在Student类中仅定义了school属性,但由于Student类直接继承自Person类, 因此Student类继承了Person类中的所有属性,也就是说,此时在Student类中,有三个属性成员,如下图所示。两个(name和age)来自于父类,一个(school)来自于当前子类。构造方法用于数据成员的初始化,但要“各司其职”,对来自于父类的数据成员需要调用父类的构造方法,例如,在第16行,使用super关键字加上对应的参数,就是调用父类的构造方法。而在第17行,来自本类的school属性,直接使用“this.school =school;”来实施本地初始化。

同样的,由于Student类直接继承自Person类,Student类中“自动”拥有父类Person类中的方法speak(),加上本身定义的study()方法和Student()构造方法,其内共有3个方法,而不是13~17行表面看到的2个方法。

第28行声明并实例化了一个Student类的对象s。第29行调用了继承自父类的speak()方法。第30行调用了Student类中的自己添加的study()方法。

提 示:在Java中只允许单继承,而不允许多重继承,也就是说一个子类只能有一个父类。但在Java中允许多层继承。

继承的限制

以上实现了继承的基本要求,但是对于继承性而言实际上也存在着若干限制,下面一一对这些限制进行说明。

限制1:Java之中不允许多重继承,但是却可以使用多层继承。

所谓的多重继承指的一个类同时继承多个父类的行为和特征功能。以下通过对比进行说明:

范例,错误的继承 —— 多重继承

从代码中可以看到,类C同时继承了类A与类B,也就是说C类同时继承了两个父类,这在Java中是不允许的,如下图所示。

然上述语法是有错误,但是在这种情况下,如果不考虑语法错误,以上这种做法的目的是希望C类同时具备A和B类的功能,所以虽然无法实现多重继承,但是却可以使用多层继承的方式来表示。所谓多层继承,是指一个类B可以继承自某一个类A,而另外一个类C又继承自B,这样在继承层次上单项继承多个类,如下图所示。

从继承图及代码中可以看到,类B继承了类A,而类C又继承了类B,也就是说类B是类A的子类,而类C则是类A的孙子类。此时,C类就将具备A和B两个类的功能,但是一般情况下,在我们所编写的代码时,多层继承的层数之中不宜超过三层。

限制2:从父类继承的私有成员,不能被子类直接使用。

子类在继承父类的时候会将父类之中的全部成员(包括属性及方法)继承下来,但是对于所有的非私有(private)成员属于显式继承,而对于所有的私有成员采用隐式继承(即对子类不可见)。子类无法直接操作这些私有属性,必须通过设置Setter和Getter方法间接操作。

限制3:子类在进行对象实例化时,从父类继承而来的数据成员需要先调用父类的构造方法来初始化,然后再用子类的构造方法来初始化本地的数据成员。

子类继承了父类的所有数据成员,同时子类也可以添加自己的数据成员。但是,需要注意的是,在调用构造方法实施数据成员初始化时,一定要“各司其职”,即来自父类的数据成员,需要调用父类的构造方法来初始化,而来自子类的数据成员初始化,要在本地构造方法中完成。在调用次序上,子类的构造方法要遵循“长辈优先”的原则,先调用父类的构造方法(生成父类对象),然后再调用子类的构造方法(生成子类对象)。也就是说,当实例化子类对象时,父类的对象会先“诞生”——这符合我们现实生活中对象存在的伦理。

限制4:被final修饰的类不能再被继承。

Java的继承性确实在某些时候可以提高程序的灵活性和代码的简洁度,但是有时我们定义了一个类但是不想让其被继承,即所有继承关系到此为止,如何实现这一目的呢?为此,Java提供了final关键字来实现这个功能。final在Java之中称为终结器。通过在类的前面添加final关键字便可以阻止该类被继承。如下例所示。

继承的限制(InheritRestrict.Java)


由于在第02行创建的父类SuperClass前用了final修饰,所以它不能被子类SubClass继承。通过上面的编译信息结果也可以看出“类型 SubClass 不能成为终态类 SuperClass 的子类”。

深度认识类的继承

子类对象的实例化过程

既然子类可以直接继承父类中的方法与属性,那父类中的构造方法是如何处理的呢?子类对象在实例化时,子类对象实例化会默认先调用父类中的无参构造函数,然后再调用子类构造方法。

请看下面的范例,并观察实例化操作流程。

子类对象的实例化(SubInstantProcess.Java)



第01~09行声明了一个Person类,在此类中设计了有一个无参构造方法Person()。实际上,构造方法主要功能是用于构造对象,初始化数据成员的,这里仅为了演示方便,在这个构造方法中输出了“***** 父类构造:1.publicPerson()”字样。

第10~17行声明了一个Student类,此类继承自Person类,它也有一个无参构造方法,并在这个构造方法中输出了"##### 子类构造:2. public Student()"字样。

第23行声明并实例化了一个Student这个子类对象s。

从程序输出结果中可以看到,虽然第23行实例化的是子类的对象,其必然调用的是子类的无参构造方法,但是父类之中的无参构造方法也被默认调用了。由此可以证明子类对象在实例化时会默认先去调用父类中的无参构造方法,之后再调用本类中的相应构造方法。

实际上,在本范例中,在子类构造方法的首行相当于默认隐含了一个“super()”语句。上面的Student类如果改写成下面的形式,也是合法的。

其中,如用户显式用super()去调用父类的构造方法,那么它必须出现在这个子类构造方法中的第1行语句。

super关键字的使用

在上面的程序中提到过super关键字,那super到底是什么?从英文本意来说,它表示“超级的”,从继承体系上,父类相对于子类是“超级的”,故此,有时候我们也称父类为超类(super-class)。

super关键字出现在子类中,而且目的是调用了父类中的构造方法,由此可以得出初步结论super主要的功能是完成子类调用父类中的内容,也就是调用父类中的属性或方法。

如果子类继承了父类的数据成员,这时就需要调用父类的有参构造方法,来初始化来自于父类的数据成员,那如何做到这一点呢?这时就需要显式的调用父类中的有参构造方法super(参数1,参数2,…)。

super调用父类中的构造方法(SuperDemo.Java)



第01~11行声明了一个名为Person的类,里面有name和age两个属性,并声明了一个含有两个参数的构造方法。

第13~22行声明了一个名为Student的类,此类继承自Person类。第17~21行声明了一个子类的构造方法Student(),在此方法中,传递了三个形参name、age和school,其中,分两个形参name和age用于super()方法,借此调用父类中有两个参数的构造方法。注意到语句“super( name, age );”是位于子类构造方法中的第一行(第19行)。第20行,用形参school本地初始化子类自己定义的数据成员school(用this.来区分同名的形参)。

第28行声明并实例化了一个Student类的对象s,并传递了3个实参用于初始化Student的类的3个数据成员(其中2个来个来自父类Person的继承,1个来自于自己类中的定义)。

提示
调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有提供这种形式的构造方法,那么在编译的时候就会报错。例如,如果我们交换了上述范例中的第19和20行代码的先后次序,就会得到如下图所示的编译错误。

事实上,super关键字不仅可用于调用父类中的构造方法,也可用于调用父类中的属性或方法,如下面的格式所示。

super.父类中的属性 ;

super.父类中的方法() ;

通过super调用父类的属性和方法(SuperDemo2.Java)。



第1~13行声明了一个名为Person的类,并声明了name和age两个属性、一个返回String类型的talk()方法,以及一个无参构造方法Person(),父类的构造方法是个空方法体,它并没有实施初始化。

第14~30行声明了一个名为Student的类,此类直接继承自Person类。

第21、22行,通过“super.属性”的方式调用父类中的name和age属性,并分别赋值。

第24行通过“super.方法名”的方式调用父类中的talk()方法,打印信息。

从程序中可以看到,子类Student可以通过super调用父类中的属性或方法。但是细心的读者在本例中可以发现,如果第21、22、25行换成this调用也是可以的,那为什么还要用super呢? super 是相对于继承而言的。super 代表的是当前类的父类,而this 这是代表当前类。如果父类的属性和方法的访问权限不是private(私有的),那么这些属性和方法在子类中是可视的,换句话说,这些属性和方法也可视为当前类所有的,那么用“this.”来访问也是理所当然的。如果子类对“父类”很“见外”,分得很清楚,那么就可用 “super.”访问来自于父类的属性和方法。

限制子类的访问

在有些时候,父类并不希望子类可以访问自己的类中全部的属性或方法,所以需要将一些属性与方法隐藏起来,不让子类去使用。为此可在声明属性或方法时加上“private”关键字,表示私有访问权限,即除了声明该属性或方法的所在类,其他外部的类均无权访问。

限制子类的访问(RestrictVisit.Java)


Student类继承自Person类,所以父类的数据(属性)成员name和age也被子类继承了,但是子类相对于父类也属于外类,在父类中,数据成员name和age的访问权限被设置为private,故子类即使继承了这个数据成员,也无法访问,它们在子类中均“不可视”,所以在第12~13行会出现编译错误。此时,即使在属性成员前加上“super.”,也不会编译成功,这体现了类的封装性。


在代码的第21行中,代码的前半部分“new Student()”创建一个无名的Student对象。一旦有了对象就可以通过“对象名.方法名()”的方式调用setVar()方法。这种创建无名对象的方式只能临时创建一个对象,使用一次后即自动销毁了。

虽然父类的私有成员,外部(包括子类)无法访问,但是在父类内部,属性和方法彼此之间是不受访问权限约束的(自己怎么能和自己见外呢?),换句话说,父类的方法可以无障碍地访问父类的任何属性和访问。

针对上述范例存在的问题,我们可以用父类的方法(例如构造方法)来访问父类的私有数据成员。请参见下面的范例。

子类访问父类的私有成员(RestrictVisit2.Java)



在第01~20行,定义了Person类,里面定义了构造方法Person()和设置属性值的方法setVar(),表面上看来,这两个方法的方法体完全相同,为什么还设置为两个不同的方法呢?其实这是有差别的。构造方法Person()仅仅是在实例化对象时自动调用(如第37行),且仅能调用一次。但如果对象诞生之后,我们想修改属性的值,那该怎么办?这时就需要一个专门的设置属性值的方法——setVar(),它可以在对象诞生后调用任意多次(如第39行)。

在第21~32行,定义了Student类,该类继承自Person类,那么Person类所有成员(包括私有的)都被“照单全收”地继承过来了,但是父类Person中被private修饰的属性成员name和age(03~04行),在子类中不能被直接访问。但是,有一个基本的原则就是,父类自己的方法可以不受限地访问父类的属性和方法。因此在Student类的构造方法中,使用“super(name, age)”(第24行)调用父类的构造方法,而父类的构造方法访问自己的属性成员是“顺理成章”的。

第27~30行,尝试定义一个test方法,并在第41行尝试调用这个方法。这是无法完成的任务,因为子类的方法尝试访问父类的私有成员——这违背了类的封装思想,故此无法通过编译,在本例中我们将这部分代码注释起来了。

由于Student类继承自Person类,所以它也很自然地继承了父类的setVar()方法和print()方法,因此,在第38~40行中,可以很自然地调用这些方法。有一个细节需要读者注意,这些方法操作的是来自父类的私有属性成员name和age。这还是体现了我们刚才提及的原则——父类自己的方法可不受限地访问自己的的属性和方法,这里的属性name、age及setVar()方法和print()方法统统来自一个类——Person,也就是说,“大家都是自己人”,自然就不能“见外”。

但是,如果我们在子类Student定义一个方法print(),如下所示。

从上面的代码可以看出,子类Student中的print()方法和父类的print()一模一样,大家可以尝试编译一下,【范例(RestrictVisit2.Java)】中的代码是无法通过编译的,这是为什么呢?这就涉及我们下面要讲到的知识点——覆写。

方法的覆写

当一个子类继承一个父类,如果子类中的方法与父类中的方法的名称、参数个数及类型且返回值类型等都完全一致时,就称子类中的这个方法覆写了父类中的方法。同理,如果子类中重复定义了父类中已有的属性,则称此子类中的属性覆写了父类中的属性。

子类Student中的print()方法和父类的print()一模一样,那么子类的print()方法就完全覆盖了父类的print()方法。而子类自己的print()方法是无法访问父类的私有属性成员的——这是封装性的体现,因此就无法通过编译。下面我们再举例说明这个概念。

子类覆写父类的实现(Override.Java)



第1~9行声明了一个名为Person的类,里面定义了name和age两个属性,并声明了一个talk()方法。

第10~26行声明了一个名为Student的类,此类继承自Person类,也就继承了name和age属性,同时声明了一个与父类中同名的talk()方法,此时Student类中的talk()方法覆写了Person类中的同名talk()方法。

第32行实例化了一个子类对象,并同时调用子类构造方法为属性赋初值。注意到name和age在父类Person中的访问权限是默认的(即没有访问权限的修饰符),那么它们在子类中是可视的,也就是说,在子类Student中,可以用“this.属性名”的方式来访问这些来自父类继承的属性成员。如果想分的比较清楚,也可以用16和17行注释部分的表示方式,即用“super.属性名”的方式来访问。

第34行用子类对象调用talk()方法,但此时调用的是子类中的talk()方法。

从输出结果可以看到,在子类Student中覆写了父类Person中的talk()方法,所以子类对象在调用talk()方法时,实际上调用的是子类中定义的方法。另外可以看到,子类的talk()方法与父类的talk()方法在声明权限时,都声明为public,也就是说这两个方法的访问权限都是一样的。

第34行调用talk()方法实际上调用的只是子类的方法,那如果的确需要调用父类中的方法,又该如何实现呢?请看下面的范例,此范例修改自上一个范例。

super调用父类的方法(Override2.Java)



第01~09行声明了一个Person类,里面定义了name和age两个属性,并声明了一个talk()方法。第10~26行声明了一个Student类,此类继承自Person,因此也继承了来自Person类的name和age属性。其中第13~19行定义了Student类的构造方法,并对数据成员实施了初始化。

由于声明了一个与父类中同名的talk()方法,因此Student类中的talk()方法覆写了Person类中的talk()方法,但在第24行通过super.talk()方式,调用了父类中的talk()方法。由于父类的talk()方法返回的是一个字符串,因此可以用连接符“+”,连接来自子类的字符串:", I am from " + this.school”,这样拼接的结果一起又作为子类的talk()方法的返回值。

第32行实例化了一个子类对象,并同时调用子类构造方法为属性赋初值。

第34行用子类对象调用talk()方法,但此时调用的是子类中的talk()方法。由于子类的talk()方法返回的是一个字符串,因此可以作为System.out.println()的参数,将字符串输出到屏幕上。

从程序中可以看到,在子类中可以通过super.方法()调用父类中被子类覆写的方法。

在完成方法的覆写时,大家应该需要注意如下几点。

⑴ 覆写的方法的返回值类型必须和被覆写的方法的返回值类型一致;

⑵ 被覆写的方法不能为static。

如果父类中的方法为静态的,而子类中的方法不是静态的,但是两个方法除了这一点外其他都满足覆写条件,那么会发生编译错误。反之亦然。即使父类和子类中的方法都是静态的,并且满足覆写条件,但是仍然不会发生覆写,因为静态方法在编译时就和类的引用类型进行匹配。

⑶ 被覆写的方法不能拥有比父类更为严格的访问控制权限。

访问权限的大小通常依据下面的次序:私有(private)<默认(default)<公有(public)。如果父类的方法使用的是public定义,那么子类覆写时,权限只能是public,如果父类的方法是default权限,则子类覆写,方法可以使用default或者是public。也就是说,子类方法的访问权限一般要比父类大,至少相等。

如果说现在父类的方法是private权限,而在子类定义的同名方法是public权限,这种方式算是方法覆写吗?覆写的本意是在父类的方法被子类所感知,但被同名的子类方法所覆盖了。如果父类之中定义的方法是private权限,那么对于子类而言根本就看不见父类的方法,因此在子类中定义的同名方法,其实相当于子类中增加了一个“全新”的方法,自然也就不存在所谓的覆写了。

属性的覆写

谓的属性覆盖指的是子类定义了和父类之中名称相同的属性。观察如下代码。

属性(数据成员)的覆写(OverrideData.java)


第01~04行,定义了类Book,其中第03行定义了一个String类型的属性info。

第05~13行,定义了类ComputerBook,它继承自类Book。在类ComputerBook中,定义了一个整型的变量info,它的名称与从父类继承而来的String类型的属性info相同(第07行)。从运行结果可以看出,在默认情况下,在不加任何标识的情况下,第10行输出的info是子类中整型的info,即100。第10行代码等价于如下代码。

由于在父类Book中,info的访问权限为默认类型(即其前面没有任何修饰符),那么在子类ComputerBook中,从父类继承而来的字符串类型的info,子类是可以感知到的,可以通过“super.父类成员”的模式访问,如第11行所示。

因此,【范例(OverrideData.java)】所示的代码并没有太大的意义,它并没有实现真正的覆写。从开发角度来说,为了满足类的封装型,类中的属性一般都需要使用private封装,一旦封装之后,子类压根就“看不见”父类的属性成员,子类定义的同名属性成员,其实就是一个“全新的”数据成员,所谓的覆写操作就完全没有意义了。

多态的实现

多态的基本概念

在深度理解多态性概念之前,请读者先回顾一下先前学习的重载概念。重载的表现形式就是调用一系列具有相同名称的方法,这些方法可根据传入参数的不同而得到不同的处理结果,这其实就是多态性的一种体现,这属于静态多态,即同一种接口,不同的实现方式。这种多态是在代码编译阶段就确定下来的。还有一种多态形式,在程序运行阶段才能体现出来,这种方式称为动态联编,也称为晚期联编(latebingding)。下面用一个范例简单地介绍一下多态的概念。

了解多态的基本概念(Poly.Java)



第01~12行声明了一个Person类,此类中定义了fun1()、fun2()两个方法。

第15~27行声明了一个Student类,此类继承自Person类,也就继承了Person类中的fun1()、fun2()方法。在子类Student中重新定义了一个与父类同名的fun1()方法,这样就达到覆写父类fun1()的目的。

第33行声明了一个Person类(父类)的对象p,之后由子类对象去实例化此对象。

第35行由父类对象调用fun1()方法。第36行由父类对象调用fun2()

从程序的输出结果中可以看到,p是父类Person的对象,但调用fun1()方法的时候并没有调用Person的fun1()方法,而是调用了子类Student中被覆写了的fun1()方法。

对于第33行的语句:Person p = new Student(),我们分析如下:在赋值运算符“=”左侧,定义了父类Person对象p,而在赋值运算符“=”右侧,用“newStudent()”声明了一个子类无名对象,然后将该子类对象赋值为父类对象p,事实上,这时发生了向上转型。本例中,展示的是一个父类仅有一个子类,这种“一对一”的继承模式,并没有体现出“多”态来。在后续文章的范例中,大家就会慢慢体会到多态中的“多”从何而来。

方法多态性

在Java中,方法的多态性体现在方法的重载,该内容已经在前面的文章中有了详细的介绍,在这里我们再用多态的眼光复习一下这部分内容,相信你会有更深入的理解。方法的多态即是通过传递不同的参数来令同一方法接口实现不同的功能。下面我们通过一个简单的方法重载的例子来了解Java方法多态性的概念。

对象多态性的使用(FuncPoly.Java)


在FuncPoly类中定义了两个名称完全一样的方法sum()(第03~08行),该接口是为了实现求和的功能,在第11和12行分别向其传递了一个和两个参数,让其计算并输出求和结果。同一个方法(方法名是相同的)能够接受不同的参数,并完成多个不同类型的运算,因此体现了方法的多态性。

对象多态性

在讲解对象多态性之前首先需要了解两个概念:向上转型和向下转型。

⑴ 向上转型。在【范例(Poly.Java)】中,父类对象通过子类对象去实例化,实际上就是对象的向上转型。向上转型是不需要进行强制类型转换的,但是向上转型会丢失精度。

⑵ 向下转型。与向上转型对应的一个概念就是“向下转型”,所谓向下转型,也就是说父类的对象可以转换为子类对象,但是需要注意的是,这时则必须要进行强制的类型转换。

以上内容可以概括成下面的两句话。

⑴ 向上转型可以自动完成。

⑵ 向下转型必须进行强制类型转换。

提示
大家注意的是,并非全部的父类对象都可以强制转换为子类对象,毕竟这种转换是不安全的。

使用多态(ObjectPoly.Java)



在第01~05行,定义了Animal类,其中定义了动物的一个公有的行为move(移动),子类Fish、Bird、Horse分别继承Animal类,并覆写了Animal类的move方法,实现各自独特的移动方式:Fish (鱼)游;Bird(鸟)跳;Horse(马)跑。

第26行声明了一个父类Animal的对象a,但没有真正实例化a。在27~29行分别实例化了三个子类对象:f、b和h。

第30~32行,通过赋值操作,将这些子类对象向上类型转换为Animal类型。然后经过父类对象a调用其move方法,这时我们发现,实际调用的却是各个子类对象的move方法。

父类对象依据被赋值的每个子类对象的类型,做出恰当的响应(即与对象具体类别相适应的反应),这就是对象多态性的关键思想。同样的消息或接口(在本例中都是move)在发送给不同的对象时,会产生多种形式的结果,这就是多态性本质。利用对象多态性,我们可以设计和实现更具扩展性的软件系统。

提示
简单来说,继承是子类使用父类的方法,而多态则是父类使用子类的方法。但更为确切来说,多态是父类使用被子类覆盖的同名方法,如果子类的方法是全新的,父类不存在同名的方法,则父类也不能使用子类自己独有的“个性化”方法。

有一点需要大家注意即使实施向上转型,父类对象所能够看见的方法依然还是本类之中所定义的方法(即被子类覆盖的方法)。如果子类扩充了一些新方法的话,那么父类对象是无法找到的。

父类对象找不到子类的扩充方法(NewMethodTest.java)



第01~07行,定义了父类A,其中包括了print()方法。第08~18行,定义了子类B,它继承自父类A,其中定义了print()方法,这样就覆写了父类的同名print()方法,此外在类B中,在第14~17行,还定义了一个新扩充的方法getB()。

在第23行,通过调用子类的构造方法“new B()”,实例化子类对象,并将其赋值给父类对象A,在第24行,从上面运行结果可以看出,父类的对象a调用了子类所定义的print()方法。

但值得我们关注的是,如果除掉第25行的注释符号“//”,就会产生如下的编译错误:“没有为类型 A 定义方法 getB()”,如下图所示。

尽管这个父类对象a的实例化依靠的是子类完成的,但是它能够看见的还是自己本类所定义的方法名称,如果方法被子类覆写了,则调用的方法体也是被子类所覆写过的方法。

如果说现在非要去调用B类的getB()方法,那么就需要进行向下转型,即将父类对象变为子类实例,向下转型是需要采用强制转换的方式完成的。

实现向下转型(DownCastTest.java)。



在第26行,将父类的对象a强制类型转换为子类对象b。对于子类添加的新方法getB(),父类依然无法找到这个方法。但是,在第26行中,对象a前面的“(B)”,表明要把a强制转换成B类型。然后将转换后结果赋给一个子类B定义的引用b,而b可以顺利找到这个getB()方法(第27行)。

从上面的几个范例分析来看,我们可以用一句话来概括这类关系,“在形式上,类定义的对象只能看到自己所属类中的成员。” 虽然通过向上类型转换,子类对象可以给父类对象赋值,但父类对象也仅能看到在子类中被覆盖的成员(这些方法也在父类定义过了),父类对象无法看到子类的新扩充方法。

隐藏

通过上面的学习,我们已经知道,当子类覆写了父类的同名方法时,如果用子类实例化父类对象,会发生向上类型转换,这时调用该方法时,会自动调用子类的方法,这是实现多态的基础,但是,在某些场景下,我们不希望父类的方法被子类方法覆写,即子类实例化后会调用父类的方法而不是子类的方法,这种情况下该怎么办?

这就需要用到另外一个概念——隐藏(Hide)。被关键词static修饰的静态方法是不能被覆盖的, Java就是利用这一个特性达到隐藏的效果。请观察下面的范例。

隐藏子类的成员。



第01~07行,定义了父类Father,里面定义了一个静态方法overWritting()。第08~14行,定义了子类Son,它继承父类Father,在这个子类中,也定义了一个与父类同名的静态方法overWritting()。第19行用子类实例化一个父类对象dad。第20行调用dad的overWritting()方法,从运行结果可以看出,这时调用的父类的方法,没有被子类所覆盖,这就是说父类“隐藏”了子类的同名方法。

而事实上,所有的静态方法都隶属于类,而非对象。所以,可以通过“类名.静态方法名”的方法来直接访问静态方法,如代码22~23行所示,从运行结果可以看出,在这样情况下,“父类”与“子类”之间的方法就不会存在谁隐藏谁的问题。在Java中,“隐藏”概念的应用并不广泛,读者了解这个概念即可。

1. 方法重载(Overload)和覆写(Override)区别(本题为常见的Java面试题)。

重载是指在相同类内定义名称相同但参数个数或类型或顺序不同的方法,而覆写是在子类当中定义名称、参数个数与类型均与父类相同的方法,用于覆写父类中的方法。具体的区别如下表所示。

在重载的关系之中,返回值类型可以不同,语法上没有错误,但是从实际的应用而言,建议,返回值类型相同。

2. this和super的区别(本题为常见的Java面试题)


由于this和super都可以调用构造方法,所以this()和super()语法不能同时出现,两者是二选一的关系。

3. final关键字的使用

final在Java之中称为终结器,在Java之中final可以修饰三类情况:修饰类、修饰方法及修饰变量。

⑴ 使用final修饰的类不能有子类(俗称太监类)。

如果父类的方法不希望被子类覆写,可在父类的方法前加上final关键字,这样该方法便不会有被覆写的机会。

⑵ 使用final定义的方法不能被子类所覆写。

在父类中,将方法设置final类型的操作,实际编程时用途并不广泛,但是在一些系统架构方面会出现比较多,这里读者知道有这类情况存在即可。

⑶ 使用final定义的变量就成为了常量

常量必须在其定义的时候就初始化(即给予赋值),这样用final修饰的变量就变成了一个常量,其值一旦确定后,便无法在后续的代码中再做修改。一般来说,为了将常量和变量区分开来,常量的命名规范要求全部字母采用大写字母方式表示。

Java—抽象类与接口

Java—核心技术类的封装、继承与多态相关推荐

  1. Java继承_Hachi君浅聊Java三大特性之 封装 继承 多态

    Hello,大家好~我是你们的Hachi君,一个来自某学院的资深java小白.最近利用暑假的时间,修得满腔java语言学习心得.今天小宇宙终于要爆发了,决定在知乎上来一场根本停不下来的Hachi君个人 ...

  2. 农夫过河算法java,Java农夫过河问题的继承与多态实现详解

    Java农夫过河问题的继承与多态实现详解 发布时间:2020-08-22 06:04:29 来源:脚本之家 阅读:61 作者:小任性嘛 题目描述: 一个农夫带着一匹狼.一只羊.一颗白菜要过河,只有一条 ...

  3. Java接口与类之间继承,多态的练习,文字游戏,

    Java接口与类之间继承,多态的练习例题,文字游戏, 问题描述: 一.定义两个接口: public interface Assaultable可攻击的, 接口有一个抽象方法public abstrac ...

  4. -1-2 java 面向对象基本概念 封装继承多态 变量 this super static 静态变量 匿名对象 值传递 初始化过程 代码块 final关键字 抽象类 接口

    java是纯粹的面向对象的语言 也就是万事万物皆是对象 程序是对象的集合,他们通过发送消息来相互通信 每个对象都有自己的由其他的对象所构建的存储,也就是对象可以包含对象 每个对象都有它的类型  也就是 ...

  5. Java面向对象三大特性(封装继承多态)解释及案例

    文章目录 包 包基本语法 命名规则 命名规范 导入包实例 访问修饰符 面向对象编程-封装 面向对象编程-继承 super关键词 super和this的比较 方法重写/覆盖 (override) 注意事 ...

  6. Java基础知识之封装+继承+多态详解

    前言 这篇博客是基于Java类和对象的基础之上的相关知识点.因为特别重要且语法规则较多,所以想单独总结方便之后复习. 本篇博客涉及知识点思维导图: 目录 1.封装 2.继承 3.多态 1.封装 生活中 ...

  7. 【Java编程进阶】封装继承多态详解

    推荐学习专栏:Java 编程进阶之路[从入门到精通] 文章目录 1. 封装 2. 继承 2.1 继承的语法 2.2 子类重写父类的方法 2.3 子类隐藏父类的方法 2.4 super 关键字 2.5 ...

  8. Java回顾 封装 继承和多态

    封装 什么是封装 封装:就是隐藏对象的属性和实现细节,仅对外提供公共访问方式. 封装时的权限控制符区别如下: 封装的意义 对于封装而言,一个对象它所封装的是自己的属性和方法,所以它是不需要依赖其他对象 ...

  9. java 继承重写_java 封装 继承 重写 多态

    封装:是指隐藏对象的属性和实现细节,仅对外提供公共访问方式. 好处: 将变化隔离.便于使用.提高重用性.提高安全性 封装原则: 将不需要对外提供的内容都隐藏起来.把属性都隐藏,提供公共方法对其访问. ...

  10. java学习笔记03-封装 继承 抽象类 多态 接口

    封装:在类的属性设置时 使用private将数据隐藏.不允许使用a.xingming来调用数据,可以设置get和set函数来对数据进行查看和修改(在其中添加if的判断语句来控制该参数的修改权限和读取权 ...

最新文章

  1. 2. 动态分配字符串
  2. java udp分别用DatagramSocket和DatagramChannel实现多计算机接收广播数据
  3. 软件工程课堂作业——寻找“水王”
  4. python arcgis批量绘图_python调用ArcGIS批量生成多环缓冲区(多边形等距离放大)...
  5. 查找字符串中第一个只出现一次的字符
  6. RHEL4-SFTP配置
  7. 前端与移动开发之vue-day1(1)
  8. ajax请求携带tooken_9 HTMLJS等前端知识系列之Ajax post请求带有token向Django请求
  9. 【codevs2301】【BZOJ2186】沙拉公主的困惑,数论练习之逆元与φ
  10. 解决自定义UITableViewCell在浏览中出现数据行重复的问题
  11. @import与link方式的区别
  12. 银行理财子公司的“超级”玩法
  13. MAC下串口助手合集
  14. 解方程(equation)
  15. Python中seek()函数的使用方法--一文读懂
  16. “兴趣爱好”,蜜糖or砒霜?
  17. 计算机学生的理想定位范文,我理想中的学校作文范文(通用3篇)
  18. 中国电信广东DNS服务器
  19. uts大学计算机排名,uts是什么大学
  20. 古诗三百首(html)

热门文章

  1. xhEditor技术手册
  2. 拆机芯片DIY一个STM32F401CCU6核心板
  3. 证券词汇集锦(中英文+注释版)
  4. LCD1602自定义符号的使用
  5. c语言单片机管脚定义,单片机-IO管脚
  6. 魔点人脸识别智慧工地实名制考勤管理系统
  7. LimeSDR GPS欺骗
  8. 如何利用FME转换空间坐标系
  9. 控制默认使用360浏览器极速模式
  10. github下载慢的两种解决方式