Java语言基础
Java市场广阔,应用广泛,且是有一定学习门槛的编程语言。掌握一门高级语言,再放言自己是开发者,是学习Java的必要理由之一。Java具备足够多的语言特性,可能比世界上最好的编程语言更值得学习。
程序结构
Java具有一些支撑其发展至今的优秀特性,包括但不限于面向对象、多线程和跨平台。
Java是纯粹的面向对象的编程语言,在Java中,类和类型是同义词。类即类型。也就是说,Java中的数据类型允许自定义。Java中有8种基本数据类型,3种复合类型。为了更好支持面向对象的特性,JDK中提供了多个包装类,用于替代8种基本类型。
关于条件语句,Java在语法上没有任何特殊。不过这里有一个地方需要稍微注意:
如果if语句后面只有一条语句,可以省略if之后的大括号,这是正确的语法规则。但如果if语句之后是一条定义变量的语句,则会编译失败。究其原因,是作用域的机制导致的问题。如果不写块级作用域的标示符号"{}",if语句后定义的变量只在该语句执行时有效,这样的变量是没有意义的,编译过程会将其视为多余代码,检查出错。
同理,在其他拥有块级作用域的语言中,也会出现类似的情况。比如执行JavaScript语句:
使用let定义变量会产生错误,而var不会,因为let是ES6定义变量的语法,var是ES5以及更早版本定义变量的语法。ES6的新特性之一,就是支持块级作用域!ES6之前只有全局作用域和函数作用域。
Java的循环结构是常见的for循环、while循环和do...while循环。
数组和循环密不可分,数组的遍历往往需要循环。Java中的数组指索引数组,自动编号。如果要实现类似PHP中的关联数组,就必须用数据结构来实现。JDK中提供了Map接口,用以保存键值对形式的信息。相应的,为了解决数组必须在定义时显式指定长度的问题,JDK提供了Collection接口。
所有实现了Iterator接口的类都可以通过forEach语句进行遍历。forEach语句和for循环语句一样是系统级别的语法规则。
面向对象
面向对象特性被Java发挥的淋漓尽致。一个文件可以包含多个类,但编译后一个类就是一个字节码文件,是平台相关的。类似C语言的main函数,通过入口函数来执行程序似乎已经是高级语言的通用特性。Java中的主方法格式为:
public为权限修饰符,用于对类或方法的权限进行访问控制;static声明此方法为静态方法;void定义方法的返回类型为无返回值;main为函数名;String[]是一个字符串数组,args是形参名;{}内为方法体。
一个有趣的练习是,对主方法定义语句中的所有关键词,分别进行去掉、替换、调换顺序的操作,关键词包括"public"、"static"、"void"、"main"、"String[]"和"args",查看程序编译和运行的结果,并思考为什么。这样的练习非常有助于认识、掌握方法定义语句的结构规则。
面向对象的逻辑是,一切事物皆对象,对象拥有属性和方法。这样的编程范式理论上可以描述并解决任何问题,因为问题也是对象。
面向对象的学习分两部分内容,一部分是关键字的功能和用法,this、static、final等,另一部分是接口、类和对象之间的关系以及与其它接口、类和对象之间的关系。这两部分内容相互交织,如extends表示继承关系,想要继承就要用extends关键字,但这两部分内容不应该混为一谈,关键字侧重语法规则,各种关系侧重程序结构。
下面分别来总结这两部分内容。
关键字
关键字分三种类型,定义类型的"interface"、"class",修饰类型的"abstract"、"final"、"static"、权限修饰符,功能类型的"extends"、"implements"、"this"、"super"、"new"、"instanceof"。
"interface"定义一个接口,接口中一般不包含方法的具体实现,因为接口是抽象的。类如果实现接口,则必须实现接口中所有的方法。但并不意味着接口中一定不允许编写方法的具体实现,Java 8提供了default方法,允许接口中使用default关键字修饰一个方法,该方法包含且必须包含方法的具体实现。被default修饰的方法不需要在实现类中单独实现,可直接调用。default方法的声明格式如下:
一些资料中"default"是权限修饰符的描述符号,表示不写其它权限修饰符的情况。这里明显看出,default和权限修饰符没有关系,它们同时存在。另外,接口中的方法权限一定是public,这是Java规定的,不可以写protected或private,如果不写,默认也是public,不同于其它场合的默认情况。
"class"定义一个类,类包含具体的属性和方法,是执行功能的主体。
"abstract"可以修饰类或方法,用于定义抽象类和抽象方法,它们都是类的模板,用于被继承。
"final"关键字修饰类,类不可被继承,修饰方法,方法不可被重写,修饰变量,变量不可被修改。
"static"可以修饰变量、方法和代码块,在类加载的"准备"阶段,虚拟机就会将static修饰的所有内容加载进内存,这一阶段虚拟机还会为变量赋予默认零值。
"extends"表示一个类继承另一个类,或一个接口继承其它接口。类是单继承的,接口允许多继承。
"implements"表示一个类实现了一些接口,可以多实现。
"this"语法灵活,可以表示类中的属性、使用this调用本类的构造方法以及表示当前对象。
"super"可调用父类构造方法、父类属性。
"new"用于实例化一个对象。
"instanceof"用于判断一个对象是否是一个类的实例,返回布尔值。
程序设计
接口、类和对象之间的排列组合使程序结构非常丰富。
接口和接口之间可以多继承;类和类之间只能单继承;对象和对象之间经常存在依赖关系。类实现一个或多个接口;对象是类的实例。
当一个类继承一个类,可能需要重写一些方法。重写需要注意的法则有两点:子类方法权限不小于父类方法、子类方法返回值类型级别不高于父类方法。
面向对象有六大程序设计原则,其中之一是里式替换原则。里式替换的含义为父类能出现的地方,子类就一定可以出现,即将父类替换为子类,程序可以正常运行。无法考证Java继承规则的设计和这一程序设计原则是否有必然的联系,但目的上确实有相似之处,能够帮助理解。如果子类方法权限比父类严格,子类方法有可能无法访问;如果返回值类型更加高级,接收父类方法返回值的变量将无法自动向下转型。
多态情况下,有一个概念叫虚方法调用。当父类方法被重写,调用父类类型的该方法,实际调用的是子类重写过的方法。
这是经典的多态情况。如果Son重写了Father中的某个方法,使用实例f调用该方法时,执行的是子类重写后的方法。相对的,如果Son中拥有Father不存在的方法,使用f调用该子类方法,编译无法通过。这一现象显得怪异,一种合理的理解是,将方法也看做变量(占用一块内存空间),一个方法就是一个变量。用Father类型定义变量f时,f的模板就已经被确定了。f拥有哪些属性和方法,已经被类型Father限定,所以当Son出现Father未拥有的方法,f无法接收。而方法重写只是改变了方法(变量)的值,可以接收。
与方法调用相应的是属性的调用。属性并不会像方法一样如果被重写,就会被父类调用。属性本身就不存在重写的说法。可能是因为方法被加载到虚拟机的方法区,而属性被加载到虚拟机的java堆区,方法区的存储机制和堆区不一样。
另一个和多态相关的设计原则是依赖倒置,意为一个类的功能应该依赖其接口,而不是依赖具体实现。这一原则基于对象的多态性,即多态。多态只有在发生继承或实现关系时才能够体现,具体表现为可以将一个类的实例,赋值给使用其父类或接口定义的变量。依赖倒置需要将实例主动提交给高层模块,即依赖注入。依赖注入有多种方式,都用到了多态的特性。
与多态规则稍显相悖的是Java的泛型机制。例如:
将String类型变量赋值给Object类型变量是可以的,但将Box赋值给Box类型,会产生incompatible types错误。事实上很容易理解,Box就是Box,无论传入的泛型参数之间有什么关系,Box和Box都不应该相容。下面是一张官网的说明图:
这里就想吐槽一下某些学习资料、博客文章了,不告诉你为什么,只告诉你这样会出错,因为这是Java的规则,用通配符"?"就能解决这个问题,实现想要的功能了。或者像下图这样:
这分明是误导。
再有与面向对象不可分割的话题是设计模式。设计模式涉及到很多接口、类和对象之间的关系,并且根据良好的关系组织,发挥出强大的力量。有时间有机会,一定认真理解一下。
应用
Java的应用部分依赖JDK。
Java的程序模块以jar包形式存在,使用import语句导入。官方提供的JDK提供了大量常用类库,可以在实际应用中不断了解、掌握它们的用法。