记录一下关于Java的几个我印象中“比较新”的内容。


定义Java数组时直接赋值:

对于一位数组:
int n[] = {1,2,3};
对于多维数组:
int n[][] = {{1,2},{3,4},{5,6}};

与之相对的是较为常用的“先定义长度,再赋值”:

int n[3] = new int[3];
n[0] = 1;
n[1] = 2;


foreach循环――“有冒号的for循环”:

这种带冒号的for循环叫做foreach循环,其在遍历数组/集合方面为开发人员提供了极大的方便,且其执行效率高于for循环。foreach语句是for语句的特殊简化版本,但其不能完全取代for语句。然而,任何foreach语句都可以改写为for语句。

for(<元素的数据类型> <元素的标识符> : <遍历对象>){
//此处为与元素标识符有关的语句,而非遍历对象
}

比如:

int[] n = {1,2,3};
for(int i : n){
    System.out.println(i);
}

对于多维数组:

int[][] n = {{1,2},{3,4},{5,6}};
for(int i : n){
    for(int j : i){
        System.out.println(j);
    }
}


面向对象概念

既然本日课上提到了面向对象的特性以及相关的概念,最后一个部分,就容我粗略地讲述一下面向对象的起源与我对面向对象的认知。

在讲正经内容之前,我还是要先谈谈教学重点的问题。老师的“试错”演示过程着实不错,确实更能让学生对代码的写法产生较为深刻的认知,但是,老师没有讲到“面向对象”概念的精髓,没有谈到编程语言的发展过程中面向对象方法产生的必然性,没有谈到面向对象方法所面对的问题,也没有谈到面向对象的几大特性对解决实际问题的意义。我的意思是,语句的写法固然是编程的一个方面,而且可以说是根基的一个方面,但是,相比于语法而言,更重要的是设计思路和对编程语言特性的理解。就此而言,我不认为这种只介绍基本概念而不展开说明,反而注重与语句的基本写法的教学是不当且失败的。相比而言,本人高中刚毕业的时候,与 Michael Lee 所师从的那位中国移动营口分公司的Java程序员老师就做得很好。对编程语言的理解到位了,对设计思路的认知到位了,面对问题时自然就会想到解决办法,自然就乐于解决更多问题,敲得多了,语句的写法也就熟悉了。

虽然老师不讲,但咱们也不能不知道是不是?毕竟也是211大学的学生,要有自学和自悟能力。

那么下面我就来浅述一下在我的认知中,编程语言的发展,以及面向对象的意义。

计算机产生之初,人们使用带孔的纸带表示二进制程序,当时计算机的特性决定了这种程序的表示模式,这就是最初的机器语言。随着计算机的进化与发展,人们有了键盘,也逐渐淘汰了使用纸带作为输入方式,与此同时,人们也在思考,如何不再单纯地使用二进制数值来表示程序,从而降低编程难度,提升编程效率和程序的可读性,就这样,汇编语言产生了。此后,计算机的型号和平台越来越多,人们又开始考虑如何进行跨硬件的编程,也就是同一套源码,可以根据不同的硬件平台进行“不同的处理”,从而可以使程序在不同的平台运行,基于这种理念,编译语言诞生了。C语言就是一种编译语言,其只需编写一套源代码,然后使用对应平台的编译器进行编译,生成机器码,就可以在这一平台上运行。但一种平台上的机器码不能在其他平台使用,如需在另一平台使用,需要使用另一平台的编译器重新编译。这时计算机所解决的问题大多都是专业性很强但所涉规模较小的问题,即大都是出于科学研究的目的,即使有个人目的而产生的程序,规模也不大。因此,这一时期的编程语言大多都是基于面向过程的理念而设计的,即以“函数(Function)”为主体,使用“函数”来处理“数据”,最后得到结果。

之后,就轮到了面向对象概念的产生。随着计算机的爆炸性发展,计算机进入千家万户,不同的人们产生了不同的使用需求,加之互联网的迅速发展,使得程序不再只面对科学研究或小规模的问题,而不得不同时面对一些琐碎且复杂的规模庞大的一些问题。以往面对过程的方法已经无法继续组织程序的设计思路,由此,面向对象(Object Oriented)的概念产生了。

面向对象是现实世界的抽象,或者说是现实世界的映射。现实世界的每一个实体(Entity),都可以抽象成对象(Object),也就是常说的“万事万物皆对象”;每一个由对象所构成的类别,都可以抽象为类(Class);各个实体之间的关系,也就是各个对象/类之间的关系,都可以通过继承关系,包含关系等得到描述;现实世界中各个实体的属性,由其所在类的属性(成员变量)来表示,各个实体的行为,由其所在类的方法(Method)来表示。通过这种方式,我们可以将整个现实世界的任何元素与编程对应起来。这种设计思想更符合世界的构成,也更符合人类的认知,同时也更具有组织性,适合解决大规模的问题。类和对象的主要意义就在于此。

基于类和对象的概念,又产生了封装。封装的意义在于对类的内部细节的隐藏与保护。从与现实世界对应的角度而言,外部力量虽然可以对一个对象进行影响,但其影响的结果终究要反映到对象本身,也就是说,本质上只有对象本身可以改变自身(莫名地感觉有种哲学的意味)。从功能上讲,将类的成员变量均设为私有,访问或修改一个对象的成员变量,必须要通过这一对象所提供的方法,这一方法可能提供了数据校验(比如限制输入的值的范围或类型)功能,这在极大程度上避免了对数据的意外破坏。尤其在大规模团队协同开发的大规模项目中,不能保证每一位开发人员都不制造“意外”,因此,这种保护是必需且十分必要的。

既然上一段提到了继承,那么就顺路浅述一下。举个浅显的例子,苹果属于一种水果,那么,水果就是苹果的父类(基类),苹果就是水果的子类(导出类),称苹果继承于水果。这种关系在现实世界中无处不在,比如自行车与汽车、火车都是交通工具,碗和盘子都是容器等等,即性质相同或相似的子类继承于同一父类。需要注意的是,飞机和鸟都能飞,但是飞机类和鸟类不能继承于“飞”这一类,因为“飞”是一个动词,表示一个动作,而非表示具体类的名词。当然,在某些情况下,飞机类和鸟类倒是可以继承与“能飞的东西”这一类的。另外,学生类不能继承于学校类,因为学校包含学生,这二者是包含关系而非继承关系。

回到继承关系上来。既然其性质相似,那么就有很大可能带有相同或相似的属性和方法,那么只需在父类中写出共有的基本属性和方法,由子类进行继承,子类就也具有了这些属性和方法,这就是继承的产生。继承不仅仅显著地减少了代码量,也在一定程度上约束了同一父类的子类的实现,对大规模开发的组织性有着不小的贡献。需要注意的是,Java 只允许单一继承,即一个子类只能显示地继承于一个父类,而 C++ 则允许多重继承(多重继承这鬼东西,想想就头疼)。Java 中的继承是允许“多级”继承的(这是我自己起的名),比如苹果继承于水果,水果继承于食物,食物又继承于XXX……最终,Java 中的所有类都直接或间接且必然地继承于一个共有的“终极”父类,叫做 Object 类。

既然子类继承了父类的属性和方法,那么就又有新的问题了:子类一般都会比父类具有更多或更为详细的性质和特点,那么,子类的这些“更”的特点要如何处理呢?就这样,重写(Override)产生了。重写就是在子类中,覆写父类的对应方法,要求子类中的方法的标识符,入口参数,返回值必须与父类相同,抛出的异常必须相同或为父类异常的子类,且被重写的父类方法不能为 private。比如,交通工具类中存在一个“移动”方法,那么在自行车类和汽车类中,则可以分别通过重写来覆盖“移动”这一方法,从而得到不同的实现。比如汽车的“移动”方法的具体实现是“四轮移动”,自行车的则是“双轮移动”。属性的覆盖也类似,如碗和盘子都继承于容器类,容器类中的“深度”属性,则可以通过子类中的赋值进行替换,如碗的“深度”属性值为“深”,盘子则为“浅”。需要强调一点,父类的 private 属性和 private 方法不会被子类继承。继承的一些限制条件和继承情况较为复杂,这里只是浅述,就不展开来说了。

提到重写,就不能不提重载(Overload)。重载是多态性的体现,指在同一个类中写一个与已有方法的标识符相同但参数不同的新的方法。当调用这组具有相同标识符的方法时,根据传入参数的不同,调用不同的方法。也就是说,同一“种”方法(体现在标识符相同)可以根据外部影响的不同(体现在传入参数的不同),执行不同的功能。这是一种便捷性特性,可以减少开发人员对各种不同方法的名称的记忆量。重载和重写有很大不同,千万不要混淆!

与继承相关的另外两个面向对象的特性,一个叫抽象(包括抽象类和抽象方法),另一个叫接口。

先说抽象(Abstract)。抽象类(Abstract Class)用于定义那些具有足够共性构成类,但是却缺乏足够的特性来创建对象的事物。抽象类一般包含了子类集合的常见方法,但是由于父类本身是抽象的,所以不能使用这些方法。因此,抽象类不能直接实例化对象,必须被其他类继承,再由这些子类实例化对象。在实际开发过程中,抽象类的主要作用是为子类提供成员变量和成员方法的模板。抽象还有一个应用,叫做抽象方法。抽象方法存在于抽象类或接口中,无方法体,其方法体必须被子类实现。

在写关于接口的内容之前,再次强调一下,以上部分描述的子类和父类(包括抽象类)的关系,均用的是“继承”。前面也提到过,Java 的继承必须是单一继承,即一个子类只能继承于一个父类,也即一个子类不能有多个父类。再次强调一下,Java 的继承只能是单一继承,Java 不存在多重继承!

终于轮到接口了……俺的 Sublime Text 3 的字数统计功能提示我,我已经写了3999个字了……呵呵,真是累死我了……

试想一下,在大规模的团队协同开发过程中,我们需要编写某些功能相同但实现不同的几个类,希望通过相似甚至相同的方式调用这些不同类中的方法,比如连接 Oracle 数据库的驱动类和连接 MySQL 数据库的驱动类。考虑到开发人员的水平良莠不齐,怎么才能强制保证这两个类的外部调用相同呢?接口(Interface)的意义就在于此。接口并不是类,尽管编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法,接口则包含类要实现的方法。接口中的方法必须是抽象方法,一个类通过“继承”接口的方式,从而来“继承”接口的抽象方法。除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。也就是说,接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。接口最大的好处就是定义了一种规范,降低了耦合性,这样就保证了实现接口的类的方法的一致性。在实际开发中,写接口的都是一堆真·大佬,都是一堆从宏观上进行系统设计,规定开发规范的“顶级程序员”和“资深程序员”。我和 Michael Lee 曾经在高中毕业之后学 Java 的时候吐槽过,写接口的程序员都是老当益壮,拄着拐棍,颤颤巍巍地走到电脑前,振臂高呼:“interface!”下面一堆小程序员闻声,立马磕头跪拜:“implement!”这一吐槽可以说是很形象地描述了接口在大规模团队开发中的重要性了,呵呵……

上一段中,与接口有关的几个“继承”都加了引号,因为严格来说,接口和类的关系并不是“继承”,而是“实现”,其关键字也不同于继承的 extends,而是实现的 implement。Java 的接口支持多重实现,即一个类可以同时实现多个接口。从不严格的意义上来说,网络上也有很多称接口的“多重实现”为“多重继承”,从理解的角度来说,倒是更容易理解,但又难免混淆继承与实现的概念。所以,我还是本着严谨的说法,认为 Java 中,类的继承只能是单一继承,而接口的实现可以是多重实现。

接口的复杂性远超初级程序员的想象,因为接口可以互相实现,可以转着圈实现,可以各种奇葩关系实现……我对接口的理解也只是停留在上述内容的阶段,更深刻的用法,包括“动态调用”在内的各种东西我都不懂,自然也就没法详说了。

也就这么多了……写的真不少……真累……