《Java核心技术》——读书笔记

Java

Java核心技术第十版源码:https://github.com/deyou123/corejava

与C++的不同

  • Java中int永远是32位的,C++的int可能是16位、32位,也可能是编译器提供商指定的其他大小,唯一的限制是不能小于short int,不能大于long int

  • Java中所有函数都属于某个类的方法(标准术语称其为方法,而不是成员函数

  • 静态成员函数(static member function),这些函数定义在类的内部,并且不对对象进行操作,Java中的main方法必须是静态的

  • Java的main方法没有为操作系统返回『退出代码』,若想返回非0值,则要调用System.exit方法

  • Java没有无符号形式的int、long、short或btye类型,整形都是有包括负数的

  • C++中的数值甚至指针都可以代替布尔值,非0值相当于布尔值的true,Java中整型值与布尔值不能相互转换,如果确实需要从布尔值转换成

  • C++会区分定义和声明,Java不会

  • Java声明一个变量后,必须用赋值语句对变量进行显示初始化,千万不要使用未初始化的变量

  • C++用const表示常量,Java用final表示常量,但是const仍然是Java中的保留字,只是没有使用

  • 类常量,static final,放在类的定义中

  • 与C或C++不同,Java不使用逗号运算符。不过,可以在for语句的第1和第3部分中使用逗号分隔表达式列表。

数据类型

浮点类型

  • float:4字节,数值后面带f或F

  • double:8字节,数值后面带d或D,或者不写(默认double)

  • 如果在数值计算中不允许有任何误差,则应该用BigDecimal类,因为浮点类型用二进制表示,精度不够

特殊值

  • Float.POSTIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN

  • Double.POSTIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN

  • 一个正整数除以0的结果是正无穷大(Double.POSTIVE_INFINITY),计算0/0或者负数的平方根的结果为NaN

强制类型转换

  • 如果想对浮点数进行舍入运算,用Math.round(double)方法,但因为该方法返回long类型,如果自动转换可能会丢失精度,建议在前面加上(int)强制类型转换。

    double x = 9.97;
    int nx = (int) Math.round(x);
    
  • 如果试图将一个数值从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,(byte)300的实际值为44。

二元赋值的强制类型转换

  • 假设x为int,则x += 3.5;是合法的,等效于x = (int) (x+3.5)

条件运算符、三元运算符

  • 如短路的if,三元运算符,均与C++保持一致

位运算符

  • 四个位运算符:&, |, ^, ~。应用在布尔值上时,&和|运算符也会得到一个布尔值。这些运算符与&&和||运算符很类似,不过&和|运算符不采用“短路”方式来求值,也就是说,得到计算结果之前两个操作数都需要计算。

  • 还有>>和<<运算符将位模式左移或右移,最后,>>>运算符会用0填充高位,这与>>不同,它会用符号位填充高位。不存在<<<运算符。

  • 移位运算符的右操作数要完成模32的运算(除非左操作数是long类型,在这种情况下需要对右操作数模64)。例如,1<<35的值等同于1<<3或8。

  • 在C/C++中,不能保证>>是完成算术移位(扩展符号位)还是逻辑移位(填充0)。实现者可以选择其中更高效的任何一种做法。这意味着C/C++>>运算符对于负数生成的结果可能会依赖于具体的实现。Java则消除了这种不确定性。

枚举类型

enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};

字符串

  • 从概念上讲,Java字符串就是Unicode字符序列

  • Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类,很自然地叫做String。每个用双引号括起来的字符串都是String类的一个实例

子串

  • String类的substring方法可以从一个较大的字符串提取出一个子串

  • substring方法的第二个参数是不想复制的第一个位置。

  • substring的工作方式有一个优点:容易计算子串的长度。字符串s.substring(a,b)的长度为b-a

    String greeting = "Hello";
    String s = greeting.substring(0, 3); // 返回Hel
    

拼接

  • 与绝大多数的程序设计语言一样,Java语言允许使用+号连接(拼接)两个字符串。

  • 当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串

  • 如果要组合多个字符串,用一个定界符分割,可以使用静态的join方法

    String all = String.join(" / ", "S", "M", "L", "XL");
    // all is the string "S / M / L / XL"
    

不可变字符串(与C/C++有较大差异)

  • String类没有提供用于修改字符串的方法。如果希望将greeting的内容修改为“Help!”,不能直接地将greeting的最后两个位置的字符修改为‘p’和‘!‘,这对于C/C++同学是非常不友好的,C++字符串是可修改的,也就是可以修改字符串的单个字符。其实Java修改字符串很简单,利用子串与拼接即可

    greeting = greeting.substring(0, 3) + "p!";
    
  • 由于不能修改Java字符串中的字符,所以在Java文档中将String类对象称为不可变字符串,如同数字3永远是数字3一样,字符串“Hello”永远包含字符H、e、l、l和o的代码单元序列,而不能修改其中的任何一个字符。当然,可以修改字符串变量greeting,让它引用另外一个字符串,这就如同可以将存放3的数值变量改成存放4一样。

  • 粗看,通过拼接来创建新字符串看起来确实效率不高,不过不可变字符串有一个显而易见的优点:编译器可以字符串共享。可以想象将各种字符串存放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。

  • 总而言之,Java的设计者认为共享带来的高效率远远胜过于提取、拼接字符串所带来的低效率。查看一下程序会发现:很少需要修改字符串,而是往往需要对字符串进行比较

  • C++程序员可能会认为Java字符串是字符型数组,char greeting[] = "Hello";,这是错的,Java字符串大致类似于char*指针,char* greeting = "Hello";

  • 当用Help!替换Java字符串变量原有的Hello时,Java代码大致进行下列操作,那原来的Hello在内存就没有变量指向了,那岂不是会引起内存泄露呢?这在C中可能是个问题,但是Java有垃圾回收

    char* temp = malloc(6);
    strncpy(temp, greeting, 3);
    strncpy(temp + 3, "p!", 3);
    greeting = temp;
    

检测字符串是否相等

  • 可以使用equals方法检测两个字符串是否相等,boolean flag = s.equals(t);,可以是字符串常量,也可以是字符串字面量,Hello.equalsgreeting);是合法的!

  • equalsIgnoreCase()方法可以不区分大消息检测是否相等

  • 一定不要使用==运算符检测了两个字符串是否相等,==是比较两者在内存的位置,两个字符串很有可能内容相同,但是在内存中地址不一样

  • C++可以放心地使用==字符串进行比较,因为重载了==运算符以便检测字符串内存的相等性,所以Java字符串变量其实很像是一个指向字符串常量的指针

  • C程序员从不使用==进行字符串比较,而是使用strcmp,Java的compareTo方法与strcmp完全类似,if (greeting.compareTo("Hello") == 0),但Java还是equals方法更加清晰一点

空串与Null串

  • 空串""是长度为0的字符串。可以调用以下代码检查一个字符串是否为空:if (str.length() == 0)if (str.equals("")),空串是一个Java对象,有自己的串长度(0)和内容(空)

  • 不过,String变量还可以存放一个特殊的值,名为null,这表示目前没有任何对象与该变量关联,要检查一个字符串是否为null,要使用以下条件:if (str == null)

  • 有时要检查一个字符串既不是null也不为空串,这种情况下就一定要两者结合使用,if (str != null && str.length() != 0),判断顺序很重要,如果在一个null值上调用方法,将会出现错误

码点与代码单元

  • Java字符串由char值序列组成。从3.3.3节“char类型”已经看到,char数据类型是一个采用UTF-16编码表示Unicode码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。

  • String的length方法将返回采用UTF-16编码表示的给定字符串所需要的代码单元数量

  • 码点数量可以使用:int cpCount = greeting.codePointCount(0, greeting.length());

  • 调用s.charAt(n)将返回位置n的代码单元,n介于0~s.length()-1之间:char first = greeting.charAt(0); // first is 'H'

Java API

java.lang.String

如果某个方法是在这个版本之后添加的,就会给出一个单独的版本号。

  • char charAt(int index)

    返回给定位置的代码单元。除非对底层的代码单元感兴趣,否则不需要调用这个方法。

  • int codePointAt(int index)5.0

    返回从给定位置开始的码点。

  • int offsetByCodePoints(int startIndex,int cpCount)5.0

    返回从startIndex代码点开始,位移cpCount后的码点索引。

  • int compareTo(String other)

    按照字典顺序,如果字符串位于other之前,返回一个负数;如果字符串位于other之后,返回一个正数;如果两个字符串相等,返回0。

  • IntStream codePoints()8

    将这个字符串的码点作为一个流返回。调用toArray将它们放在一个数组中。

  • new String(int[]codePoints,int offset,int count)5.0

    用数组中从offset开始的count个码点构造一个字符串。

  • boolean equals(Object other)

    如果字符串与other相等,返回true。

  • boolean equalsIgnoreCase(String other)

    如果字符串与other相等(忽略大小写),返回true。

  • boolean startsWith(String prefix)

  • boolean endsWith(String suffix)

    如果字符串以suffix开头或结尾,则返回true。

  • int index0f(String str)

  • int index0f(int cp)

  • int index0f(int cp,int fromIndex)

    返回与字符串str或代码点cp匹配的第一个子串的开始位置。这个位置从索引0或fromIndex开始计算。如果在原始串中不存在str,返回-1。

  • int lastIndex0f(String str)

  • int lastIndex0f(String str,int fromIndex)

  • int lastindex0f(int cp)

  • int lastindex0f(int cp,int fromIndex)

    返回与字符串str或代码点cp匹配的最后一个子串的开始位置。这个位置从原始串尾端或fromIndex开始计算。

  • int length()

    返回字符串的长度。

  • int codePointCount(int startIndex,int endIndex)5.0

    返回startIndex和endIndex-1之间的代码点数量。没有配成对的代用字符将计入代码点。

  • String replace(CharSequence oldString,CharSequence newString)

    返回一个新字符串。这个字符串用newString代替原始字符串中所有的oldString。可以用String或StringBuilder对象作为CharSequence参数。

  • String substring(int beginIndex)

  • String substring(int beginIndex,int endIndex)

    返回一个新字符串。这个字符串包含原始字符串中从beginIndex到串尾或或endIndex–1的所有代码单元。

  • String toLowerCase()

  • String toUpperCase()

    返回一个新字符串。这个字符串将原始字符串中的大写字母改为小写,或者将原始字符串中的所有小写字母改成了大写字母。

  • String trim()

    返回一个新字符串。这个字符串将删除了原始字符串头部和尾部的空格。

  • String join(CharSequence delimiter,CharSequence...elements)8

    返回一个新字符串,用给定的定界符连接所有元素。

StringBuilder类

  • 每次连接字符串,都会构建一个新的String对象,既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。

    StringBuilder builder = new StringBuilder();
    builder.append(ch); // appends a single character
    builder.append(str); // appends a string
    String completedString = builder.toString(); // 最后获得String对象
    
  • 在JDK5.0中引入StringBuilder类。这个类的前身是StringBuffer,其效率稍有些低,但允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑(通常都是这样),则应该用StringBuilder替代它。这两个类的API是相同的。java.lang.StringBuilder 5.0

  • StringBuilder()

    构造一个空的字符串构建器。

  • int length()

    返回构建器或缓冲器中的代码单元数量。

  • StringBuilder append(String str)

    追加一个字符串并返回this。

  • StringBuilder append(char c)

    追加一个代码单元并返回this。

  • StringBuilder appendCodePoint(int cp)

    追加一个代码点,并将其转换为一个或两个代码单元并返回this。

  • void setCharAt(int i,char c)

    将第i个代码单元设置为c。

  • StringBuilder insert(int offset,String str)

    在offset位置插入一个字符串并返回this。

  • StringBuilder insert(int offset,Char c)

    在offset位置插入一个代码单元并返回this。

  • StringBuilder delete(int startIndex,int endIndex)

    删除偏移量从startIndex到-endIndex-1的代码单元并返回this。

  • String toString()

    返回一个与构建器或缓冲器内容相同的字符串。

读取输入

Scanner类定义在java.util包中。当使用的类不是定义在基本java.lang包中时,一定要使用import指示字将相应的包加载进来

因为输入是可见的,所以Scanner类不适用于从控制台读取密码。Java SE 6特别引入了Console类实现这个目的

java.util.Scanner 5.0

  • Scanner(InputStream in)

    用给定的输入流创建一个Scanner对象。

  • String nextLine()

    读取输入的下一行内容。

  • String next()

    读取输入的下一个单词(以空格作为分隔符)。

  • int nextInt()

  • double nextDouble()

    读取并转换下一个表示整数或浮点数的字符序列。

  • boolean hasNext()

    检测输入中是否还有其他单词。

  • boolean hasNextInt()

  • boolean hasNextDouble()

    检测是否还有表示整数或浮点数的下一个字符序列。

文件输入与输出

  • 要想对文件进行读取,就需要一个用File对象构造一个Scanner对象,如下所示:Scanner in = new Scanner(Paths.get("myfile.txt"), "UTF-8");

    如果文件名中包含反斜杠符号,就要记住在每个反斜杠之前再加一个额外的反斜杠:“c:\mydirectory\myfile.txt”。

  • 要想写入文件,就需要构造一个PrintWriter对象。在构造器中,只需要提供文件名:PrintWriter out = new PrintWriter("myfile.txt", "UTF-8");,如果文件不存在,创建该文件。可以像输出到System.out一样使用print、println以及printf命令

控制流程

  • Java的控制流程结构与C和C++的控制流程结构一样,只有很少的例外情况。没有goto语句,但break语句可以带标签,可以利用它实现从内层循环跳出的目的(这种情况C语言采用goto语句实现)

块作用域

  • 块(即复合语句)是指由一对大括号括起来的若干条简单的Java语句。块确定了变量的作用域。一个块可以嵌套在另一个块中

  • 但是,不能在嵌套的两个块中声明同名的变量,否则会无法通过编译。在C++中,可以在嵌套的块中重定义一个变量。在内层定义的变量会覆盖在外层定义的变量。这样,有可能会导致程序设计错误,因此在Java中不允许这样做。

switch语句

  • 在处理多个选项时,使用if/else结构显得有些笨拙。Java有一个与C/C++完全一样的switch语句。

  • 编译代码时可以考虑加上-Xlint:fallthrough选项,javac --Xlint:fallthrough Test.java,这样一来,如果某个分支最后缺少一个break语句,编译器就会给出一个警告消息。

  • case标签可以是:

    • 类型为char、byte、short或int的常量表达式。

    • 枚举常量。

    • 从Java SE 7开始,case标签还可以是字符串字面量

中断控制流程语句

  • 尽管Java的设计者将goto作为保留字,但实际上并没有打算在语言中使用它。通常,使用goto语句被认为是一种拙劣的程序设计风格。

  • 与C++不同,Java还提供了一种带标签的break语句,用于跳出多重嵌套的循环语句。有时候,在嵌套很深的循环语句中会发生一些不可预料的事情。此时可能更加希望跳到嵌套的所有循环语句之外。通过添加一些额外的条件判断实现各层循环的检测很不方便。只能跳出语句块,而不能跳入语句块。

    read_data:
    while () {
        ...
        for (...) {
            if (n < 1) {
                break read_data;
            }
        }
    }
    
  • Java的continue同理

大数值

  • 如果基本的整数和浮点数精度不能够满足需求,那么可以使用java.math包中的两个很有用的类:BigInteger和BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算。

  • 使用静态的valueOf方法可以将普通的数值转换为大数值:BigInteger a = BigInteger.valueOf(100);

  • 遗憾的是,不能使用人们熟悉的算术运算符(如:+和*)处理大数值。而需要使用大数值类中的add和multiply方法。

  • 与C++不同,Java没有提供运算符重载功能。程序员无法重定义+和*运算符,使其应用于BigInteger类的add和multiply运算。Java语言的设计者确实为字符串的连接重载了+运算符,但没有重载其他的运算符,也没有给Java程序员在自己的类中重载运算符的机会。

API java.math.BigInteger 1.1

  • BigInteger add(BigInteger other)

  • BigInteger subtract(BigInteger other)

  • BigInteger multiply(BigInteger other)

  • BigInteger divide(BigInteger other)

  • BigInteger mod(BigInteger other)

    返回这个大整数和另一个大整数other的和、差、积、商以及余数。

  • int compareTo(BigInteger other)

    如果这个大整数与另一个大整数other相等,返回0;如果这个大整数小于另一个大整数other,返回负数;否则,返回正数。

  • static BigInteger valueOf(long x)

    返回值等于x的大整数。

java.math.BigInteger 1.1

  • BigDecimal add(BigDecimal other)

  • BigDecimal subtract(BigDecimal other)

  • BigDecimal multiply(BigDecimal other)

  • BigDecimal divide(BigDecimal other,RoundingMode mode)5.0

    返回这个大实数与另一个大实数other的和、差、积、商。要想计算商,必须给出舍入方式(rounding mode)。RoundingMode.HALF_UP是在学校中学习的四舍五入方式(即,数值0到4舍去,数值5到9进位)。它适用于常规的计算。有关其他的舍入方式请参看API文档。

  • int compareTo(BigDecimal other)

    如果这个大实数与另一个大实数相等,返回0;如果这个大实数小于另一个大实数,返回负数;否则,返回正数。

  • static BigDecimal valueOf(long x)

  • static BigDecimal valueOf(long x,int scale)

    cv返回值为x或x/10scale的一个大实数。

数组

  • 数组是一种数据结构,用来存储同一类型值的集合。通过一个整型下标可以访问数组中的每一个值。例如,如果a是一个整型数组,a[i]就是数组中下标为i的整数。

  • 创建一个数字数组时,所有元素都初始化为0。boolean数组的元素会初始化为false。对象数组的元素则初始化为一个特殊值null,这表示这些元素(还)未存放任何对象。

  • 一旦创建了数组,就不能再改变它的大小(尽管可以改变每一个数组元素)。如果经常需要在运行过程中扩展数组的大小,就应该使用另一种数据结构——数组列表(array list)

    int[] a; // 声明
    int[] a = new int[100]; // 这条语句创建了一个可以存储100个整数的数组,等同于C++的int *a = new int[100];
    int len = a.length; // 获得数组中元素个数,注意length不是方法,而是成员变量
    int[] smallPrimes = {2, 3, 5, 7, 11, 13}; // 创建对象数组并初始化
    smallPrimes = new int[] {17, 19}; // 在不创建新变量的情况下重新初始化一个数组
    
  • 在Java中,允许数组长度为0。在编写一个结果为数组的方法时,如果碰巧结果为空,则这种语法形式就显得非常有用。注意,数组长度为0与null不同。此时可以创建一个长度为0的数组:

    new elementType[0]
    

foreach循环

  • foreach循环定义一个变量用于暂存集合中的每一个元素,并执行相应的语句(当然,也可以是语句块)。collection这一集合表达式必须是一个数组或者是一个实现了Iterable接口的类对象(例如ArrayList)

    for (variable : collection) statement
    
  • for each循环语句显得更加简洁、更不易出错(不必为下标的起始值和终止值而操心)

  • 有个更加简单的方式打印数组中的所有值,即利用Arrays类的toString方法。调用Arrays.toString(a),返回一个包含数组元素的字符串,这些元素被放置在括号内,并用逗号分隔,例如,“[2,3,5,7,11,13]

数组拷贝

  • 将一个数组变量拷贝给另一个数组变量。这时,两个变量将引用同一个数组:

    int[] luckyNumbers = smallPrimes;
    luckNumbers[5] = 12; // now smallPrimes[5] is also 12
    
  • 如果希望将所有值都拷贝到一个新的数组里面,则要使用Array类的copyOf方法,第二个参数是新数组的长度,如果长度更大,那么多余元素则赋值默认值,如果长度更小,则只拷贝前面的元素

    int[] copiedLuckyNumbers = Arrays.copyOf(luckNumbers, luckNumbers.length);
    

命令行参数

  • 在Java应用程序的main方法中,程序名并没有存储在args数组中

    java Message -h world # args[0]是'-h',而不是'Message'或'java',args[1]是'world'
    Message -h world # C++可执行二进制文件,args[0]是`Message`
    

数组API

java.util.Arrays 1.2

  • static String toString(type[]a)5.0

    返回包含a中数据元素的字符串,这些数据元素被放在括号内,并用逗号分隔。

    参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

  • static type copyOf(type[]a,int length)6

  • static type copyOfRange(type[]a,int start,int end)6

    返回与a类型相同的一个数组,其长度为length或者end-start,数组元素为a的值。

    参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

    start 起始下标(包含这个值)。

    end 终止下标(不包含这个值)。这个值可能大于a.length。在这种情况下,结果为0或false。

    length 拷贝的数据元素长度。如果length值大于a.length,结果为0或false;否则,数组中只有前面length个数据元素的拷贝值。

  • static void sort(type[]a)

    采用优化的快速排序算法对数组进行排序。

    参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

  • static int binarySearch(type[]a,type v)

  • static int binarySearch(type[]a,int start,int end,type v)6

    采用二分搜索算法查找值v。如果查找成功,则返回相应的下标值;否则,返回一个负数值r。-r-1是为保持a有序v应插入的位置。

    参数:a 类型为int、long、short、char、byte、boolean、float或double的有序数组。

    start 起始下标(包含这个值)。

    end 终止下标(不包含这个值)。

    v 同a的数据元素类型相同的值。

  • static void fill(type[]a,type v)

    将数组的所有数据元素值设置为v。

    参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。
    v 与a数据元素类型相同的一个值。

  • static boolean equals(type[]a,type[]b)

    如果两个数组大小相同,并且下标相同的元素都对应相等,返回true。

    参数:a、b 类型为int、long、short、char、byte、boolean、float或double的两个数组。

多维数组

  • 二维数组

    double[][] balances; // 声明
    balances = new double[NYEARS][NRATES] // 初始化
    int[][] = magicSquare = { // 直接初始化
        {16,3,2,13},
        {5,10,,11,8},
        {9,6,7,12},
        {4,15,14,1}
    };
    
  • for each循环语句不能自动处理二维数组的每一个元素。它是按照行,也就是一维数组处理的。要想访问二维数组a的所有元素,需要使用两个嵌套的循环,

  • 要想快速地打印一个二维数组的数据元素列表,可以调用

    System.out.println(Arrays.deepToString(a));
    

不规则数组

  • 实际上Java没有多维数组的概念,只有一维数组,多维数组可以理解为数组的数组

  • Java的二维数组相当于C++的:

    double** balances = new double*[10]; // 一个包含是个指针的数组
    for (int i = 0; i < 10; i++) {
        balances = new double[6]; // 指针数组的每一个元素被填充了一个包含6个数字的数组
    }
    

对象和类

在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。

OOP与类

对象与对象变量

  • new Date()表达式构造了一个新对象,构造的对象如果要多次使用,可以存放在一个变量中,Data birthday = new Date();

  • 对象变量定义后,但没有引用具体的对象,所以调用该对象变量的方法将会编译报错,这时可以new一个对象初始化这个变量,也可以引用一个已存在的对象(变量),这时两个变量引用一个对象

  • 一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。

  • 在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用

  • 可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象。

  • 如果将一个方法应用于一个值为null的对象上,那么就会产生运行时错误。局部变量不会自动地初始化为null,而必须通过调用new或将它们设置为null进行初始化

  • Java中的对象引用其实相当于C++的对象指针,C++没有空引用。在Java中的null引用对应C++中的NULL指针。

  • 所有的Java对象都存储在堆中。当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针。

  • 在C++中,指针十分令人头疼,并常常导致程序错误。稍不小心就会创建一个错误的指针,或者造成内存溢出。在Java语言中,这些问题都不复存在。如果使用一个没有初始化的指针,运行系统将会产生一个运行时错误,而不是生成一个随机的结果。同时,不必担心内存管理问题,垃圾收集器将会处理相关的事宜。

  • 在Java中,必须使用clone方法获得对象的完整拷贝。

更改器方法与访问器方法

  • 访问对象且有可能修改对象的方法叫更改器方(mutator method),只访问对象而不修改对象的方法有时称为访问器方法(accessor method)。

  • 在C++中,带有const后缀的方法是访问器方法;默认为更改器方法。但是,在Java语言中,访问器方法与更改器方法在语法上没有明显的区别。

构造器

  • 构造器与类同名。在构造Employee类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。

  • 构造器与其他的方法有一个重要的不同。构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的

  • Java构造器的工作方式与C++一样。但是,要记住所有的Java对象都是在堆中构造的,构造器总是伴随着new操作符一起使用。C++程序员最易犯的错误就是忘记new操作符:

    Employee number007("James Bond", 1000, 1950); // C++, not Java
    new Employee number007("James Bond", 1000, 1950); // Java
    
  • 警告:请注意,不要在构造器中定义与实例域重名的局部变量。

隐式参数与显式参数

  • 隐式(implicit)参数,是出现在方法名前的Employee类对象。显式参数是明显地列在方法声明中的参数。

  • 在每一个方法中,关键字this表示隐式参数

    public void raiseSalary(double byPercent) {
        double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }
    
  • 在C++中,通常在类的外面定义方法,如果在类的内部定义方法,这个方法将自动地成为内联(inline)方法。

  • 在Java中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法。是否将某个方法设置为内联方法是Java虚拟机的任务。即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法。

  • C++也有同样的原则。方法可以访问所属类的私有特性(feature),而不仅限于访问隐式参数的私有特性。

final实例域

  • 可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。

  • final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。

  • 但是final关键字对于可变的类可能会引起歧义:

    class Employee {
        public Employee () { evaluation = new StringBuilder();}
        private final StringBuilder evaluations;
        public void giveColdStar() {
            evaluation.append(LocalDate.now() + ": Cold star!\n");
        }
    
    }
    

static静态域

  • 如果将域定义为static,1000个类的对象,也只有这一个static域,因为它是属于类的,而不属于任何独立的对象。举例,下面的nextId维护了Employee类的static域,所有对象都可以访问,且唯一,所以用来做全局唯一的ID值,是非常合适的

    class Employee {
        private static int nextId = 1;
        private int id;
        public void setId() {
            id = nextId;
            nextId++;
        }
    }
    
    

静态常量

  • static final xxx,System.out是一个典型的静态常量

    public class Math {
        public static final double PI = 3.1415926;
    }
    
    int 2pi = 2*Math.PI; // 程序中可以直接用Math.PI访问static域
    

静态方法

  • 静态方法是一种不能向对象实施操作的方法。例如,Math类的pow方法就是一个静态方法。Math.pow(x,a),不使用任何Math对象,换句话说,没有隐式参数

  • 静态方法不能访问非静态域,但可以访问自身类中的静态域,可以通过类名调用静态方法,

  • 两种情况使用静态方法比较好

    • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。

    • 一个方法只需要访问类的静态域(例如:Employee.getNextId)。

      class Employee {}
          ...
          public static int getNextId() {
              return nextId;
          }
      }
      
      int n = Employee.getNextId();
      

C++注释

  • Java中的静态域与静态方法在功能上与C++相同。但是,语法书写上却稍有所不同。在C++中,使用::操作符访问自身作用域之外的静态域和静态方法,如Math::PI

  • 术语“static”有一段不寻常的历史。起初,C引入关键字static是为了表示退出一个块后依然存在的局部变量。在这种情况下,术语“static”是有意义的:变量一直存在,当再次进入该块时仍然存在。随后,static在C中有了第二种含义,表示不能被其他文件访问的全局变量和函数。为了避免引入一个新的关键字,关键字static被重用了。最后,C++第三次重用了这个关键字,与前面赋予的含义完全不一样,这里将其解释为:属于类且不属于类对象的变量和函数。这个含义与Java相同。

静态工厂

  • 静态方法还有另外一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。

main方法

  • main方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。

  • 提示:每一个类可以有一个main方法。这是一个常用于对类进行单元测试的技巧。

    class Employee {
        ...
        public static void main (String[] args) { // unit test
            ...
        }
    }
    class Application {
        ...
        public static void main (String[] args) { // main entrance
            ...
        }
    }
    
    # 独立测试
    java Employee
    # 运行程序,Employee的main方法永远不会执行
    java Application
    

方法参数

  • Java程序设计语言总是采用按值调用(call by value)。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

  • 方法参数共有两种类型:

    • 基本数据类型(数字、布尔值)。所以一个方法不能修改数值型或布尔型的参数

    • 对象引用。一个方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。

    • 读者已经看到,一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了,比如

      public static void tripleSalary(Employee x) {
          x.raiseSalary(200);
      }
      harry = new Employee(...);
      tripleSalary(harry);
      

      具体的执行过程为:

      1)x被初始化为harry值的拷贝,这里是一个对象的引用。

      2)raiseSalary方法应用于这个对象引用。x和harry同时引用的那个Employee对象的薪金提高了
      200%。

      3)方法结束后,参数变量x不再使用。当然,对象变量harry继续引用那个薪金增至3倍的雇员对象

    • 所以,一个方法可以改变一个对象参数的状态,一个方法不能让对象参数引用一个新的对象。

  • C++注释:C++有值调用和引用调用。引用参数标有&符号。例如,可以轻松地实现void tripleValue(double&x)方法或void swap(Employee&x,Employee&y)方法实现修改它们的引用参数的目的。

对象构造

重载

  • Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。注意返回类型不是签名的一部分

默认域初始化

  • 如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。然而,只有缺少程序设计经验的人才会这样做。确实,如果不明确地对域进行初始化,就会影响程序代码的可读性。

  • 这是域与局部变量的主要不同点。必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的域,将会被自动初始化为默认值(0、false或null)。

无参/默认构造方法

  • 很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时,其状态会设置为适当的默认值

  • 如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。

  • 如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。

  • 如果确实想调用无参构造器,那么程序员就得显示提供一个默认的无参构造器,

显式域初始化

  • Java中的域初始值不一定是常量值,也可以是调用常量来初始化,这是和C++很大的区别

    class Employee {
        private static int nextId;
        private int id = assignId();
        private static int assignId() {
            int r = nextId;
            nextId++;
            return r;
        }
    }
    
    
  • C++注释:在C++中,不能直接初始化类的实例域。所有的域必须在构造器中设置。但是,有一个特殊的初始化器列表语法,如下所示:

    Employee::Employee(String n, double s) : name(n), salary(s) {
    
    }
    

参数名

  • 参数变量用同样的名字将实例域屏蔽起来,但可以加上this访问实例域

    public Employee(String n, double salary) {
        this.name = name;
        this.salary = salary;
    }
    
  • C++注释:在C++中,经常用下划线或某个固定的字母(一般选用m或x)作为实例域的前缀。Java程序员一般不这么做

构造器调用另一个构造器

  • Java可以在构造器内部使用this调用另一个构造器

    public Employee(String n, double salary) {
        this("Employee @" + nextId, s);
        nextId++;
    }
    
    
  • 采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。

  • C++注释:在Java中,this引用等价于C++的this指针。但是,在C++中,一个构造器不能调用另一个构造器。在C++中,必须将抽取出的公共初始化代码编写成一个独立的方法。

对象析构与finalize方法

  • 由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。

  • 可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。

包(package)

  • 使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。

  • 从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util包与java.util.jar包毫无关系。每一个都拥有独立的类集合。

导入

  • 但是,需要注意的是,只能使用星号(*)导入一个包,而不能使用import java.*import java.*.*导入以java为前缀的所有包。

  • C++注释:C++程序员经常将import与#include弄混。实际上,这两者之间并没有共同之处。在C++中,必须使用#include将外部特性的声明加载进来,这是因为C++编译器无法查看任何文件的内部,除了正在编译的文件以及在头文件中明确包含的文件。Java编译器可以查看其他文件的内部,只要告诉它到哪里去查看就可以了。

  • 在Java中,通过显式地给出包名,如java.util.Date,就可以不使用import;而在C++中,无法避免使用#include指令

  • Import语句的唯一的好处是简捷。可以使用简短的名字而不是完整的包名来引用一个类。例如,在import java.util.*(或import java.util.Date)语句之后,可以仅仅用Date引用java.util.Date类。
    在C++中,与包机制类似的是命名空间(namespace)。在Java中,package与import语句类似于C++中的namespace和using指令。

静态导入

  • import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。

    import static Math;
    sqrt(pow(x,2) + pow(y,2));
    // if no static import
    Math.sqrt(Math(x,2) + Math(y,2));
    
    

将类放入包中

  • 要想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。

    package com.xxx.xxx;
    public class Emplyee
    ...
    
  • 如果没有在源文件中放置package语句,这个源文件中的类就被放置在一个默认包(defaulf package)中。默认包是一个没有名字的包。

包作用域

  • 标记为public的部分可以被任意的类使用;标记为private的部分只能被定义它们的类使用。如果没有指定public或private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。

类设计技巧

  1. 一定要保证数据私有

  2. 一定要对数据初始化

  3. 不要在类中使用过多的基本类型

  4. 不是所有的域都需要独立的域访问器和域更改器

  5. 将职责过多的类进行分解

  6. 类名和方法名要能够体现它们的职责
    命名类名的良好习惯是采用一个名词(Order)、前面有形容词修饰的名词(RushOrder)或动名词(有“-ing”后缀)修饰名词(例如,BillingAddress)。对于方法来说,习惯是访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。

  7. 优先使用不可变的类

继承

  • 继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。

  • 反射(reflection)是指在程序运行期间发现更多的类及其属性的能力

  • "is-a”关系是继承的一个明显特征

  • Java没有多继承

超类与子类

  • extends关键字表示继承

    public class Manager extends Employee {}
    
  • C++注释:Java与C++定义继承类的方式十分相似。Java用关键字extends代替了C++中的冒号(:)。在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承。

  • 关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。超类和子类是Java程序员最常用的两个术语,而了解其他语言的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。

  • 前缀“超”和“子”来源于计算机科学和数学理论中的集合语言的术语。所有雇员组成的集合包含所有经理组成的集合。可以这样说,雇员集合是经理集合的超集,也可以说,经理集合是雇员集合的子集。

  • 在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

覆盖(override)

  • 子类可以定义同名方法来覆盖超类的方法

  • super关键字指示编译器调用超类方法,注释:有些人认为super与this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

    public class Manager extneds Employee {
        private double bonus;
        public double getSalary() {
            double salary = super.getSalary();
            return baseSarary + bonus;
        }
    }
    
  • 正像前面所看到的那样,在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。

  • C++注释:在Java中使用关键字super调用超类的方法,而在C++中则采用超类名加上::操作符的形式。例如,在Manager类的getSalary方法中,应该将super.getSalary替换为Employee::getSalary。

子类构造器

  • 由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。

    public Manager(String name, double salary) {
        super(name, salary);
        bonus = 0;
    }
    
  • 如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。

  • 回忆一下,关键字this有两个用途:一是引用隐式参数,二是调用该类其他的构造器。同样,super关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候,这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

  • C++注释:在C++的构造函数中,使用初始化列表语法调用超类的构造函数,而不调用super。在C++中,Manager的构造函数如下所示:

    Manager::Manager(String name, double salary) : Employee(name, salary) {
        bonus = 0;
    }
    

多态

  • is-a”规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。

  • 在Java程序设计语言中,对象变量是多态的。一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象(例如,Manager、Executive、Secretary等)

    Employee e;
    e = new Employee(..);
    e = new Manager(..);
    
    Manager boss = new Manager(..);
    Employee[] staff = new Employee[3];
    staff[0] = boss;
    boss.setBonus(500); // ok
    staff[0].setBonus(500); // error,staff[0]声明的是Employee
    Manager m = staff[i]; // error,不能将超类的引用赋给子类变量,不是所有的员工都是经理
    

理解方法调用(重要!)

  • 弄清楚如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:

    • 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。至此,编译器已获得所有可能被调用的候选方法。

    • 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int)。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。至此,编译器已获得需要调用的方法名字和参数类型。

      • 因为方法返回类型不影响方法签名,所以允许子类将覆盖方法定义为原返回类型的子类型

        public Employee getBuddy() {}
        public Manager getBuddy() {} // 重载方法
        
        
    • 如果是private方法、static方法、final方法(有关final修饰符的含义将在下一节讲述)或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们列举的示例中,编译器采用动态绑定的方式生成一条调用f(String)的指令。

    • 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。

      • 每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,以便寻找与调用f(Sting)相匹配的方法。这个方法既有可能是D.f(String),也有可能是X.f(String),这里的X是D的超类。这里需要提醒一点,如果调用super.f(param),编译器将对隐式参数超类的方法表进行搜索。
  • 在运行时,调用e.getSalary()的解析过程为:

    • 首先,虚拟机提取e的实际类型的方法表。既可能是Employee、Manager的方法表,也可能是Employee类的其他子类的方法表。

    • 接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法。

    • 最后,虚拟机调用方法。

  • 动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个Executive类的对象,就会自动地调用Executive.getSalary()方法。

  • 警告:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。经常会发生这类错误:在声明子类方法的时候,遗漏了public修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。

阻止继承:final类和方法

  • 有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。例如,假设希望阻止人们定义Executive类的子类,就可以在定义这个类的时候,使用final修饰符声明。声明格式如下所示:

    public final class Excutive extends Manager {}
    
  • 类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)

    public class Employee {
        public final String getName() {}
    }
    
  • 前面曾经说过,域也可以被声明为final。对于final域来说,构造对象之后就不允许改变它们的值了。不过,如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域。

  • 将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。

  • C++注释:C++默认所有方法都不具有多态性,而Java反过来,作者提倡在两者之间寻找一个平衡

强制类型转换

  • 正像有时候需要将浮点型数值转换成整型数值一样,有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。

  • 进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。

  • 将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检查。

  • 在将超类转换成子类之前,应该使用instanceof进行检查。如果变量是null,用instanceof检查也不会产生异常

    if (staff[1] instanceof Manager) {
        boss = (Manger) staff[1];
    }
    
  • Java的强制类型转换转换很像像C++的dynamic_cast操作,它们之间只有一点重要的区别:当类型转换失败时,Java不会生成一个null对象,而是抛出一个异常。从这个意义上讲,有点像C++中的引用(reference)转换。真是令人生厌。在C++中,可以在一个操作中完成类型测试和类型转换。

    Manager* boss = dynamic_cast(staff[1]); // C++
    if (boss != NULL) ...
    

    在Java中,需要将instanceof运算符和类型转换组合起来使用:

    if (staff[1] instanceof Manager) {
        boss = (Manger) staff[1];
    }
    

抽象类

  • 为了提高程序的清晰度,人们只将抽象类作为派生其他类的基类,而不作为想使用的特定的实例类。包含一个或多个抽象方法的类本身必须被声明为抽象的。但注意抽象类内部是可以有具体数据和具体方法的

    // 抽象方法
    public abstract String getDescription();
    // 抽象类
    public abstract class Person {}
    
  • 类即使不含抽象方法,也可以将类声明为抽象类。

  • 抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象

  • 在C++中,有一种在尾部用=0标记的抽象方法,称为纯虚函数。只要有一个纯虚函数,这个类就是抽象类。在C++中,没有提供用于表示抽象类的特殊关键字。

受保护访问

  • 大家都知道,最好将类中的域标记为private,而方法标记为public。任何声明为private的内容对其他类都是不可见的。前面已经看到,这对于子类来说也完全适用,即子类也不能访问超类的私有域。

  • 然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为protected。

  • C++注释:事实上,Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。这与C++中的保护机制稍有不同,Java中的protected概念要比C++中的安全性差。

  • Java用于控制可见性的4个访问修饰符:

    • 仅对本类可见——private。

    • 对所有类可见——public。

    • 对本包和所有子类可见——protected。

    • 对本包可见——默认(很遗憾),不需要修饰符。

Object: 所有类的超类

  • Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来的。如果一个类没有明确指出超类,Object类就是这个类的超类

  • C++注释:在C++中没有所有类的根类,不过,每个指针都可以转换成void*指针。

equals方法

  • Object.equals(a,b)方法:如果两个参数都为null,则返回true;如果一个为null,则返回false,如果两个都不为null,则调用a.equals(b)

  • 在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。

  • 一个比较好的equals方法

    public class Employee {
        public boolean equals(Object otherObject) { // 注意显式参数是Object类
            // quick test
            if (this == otherObject) return true;
            // must return false if the explicit parameter is null
            if (otherObject == null) return false;
            // if class don't match, they can't be equal
            if (getClass() != otherObject.getClass()) return false;
            // now we know otherObject is a non-null Employee
            Employee other = (Employee) otherObject;
            // 为了防备name或hireDay可能为null的情况,需要使用Objects.equals方法
            return Object.equals(name, other.name) && salary == other.salary && Obejct.equals(hireDay, other.hireDay);
        }
    }
    
    

equals: 相等测试和继承

  • 有些程序员喜欢在equals方法中调用instanceof方法,但是这有一些麻烦,比如Java语言规范要求equals方法具有以下特性

    • 自反性:对于任何非空引用x,x.equals(x)应该返回true。

    • 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。

    • 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true。

    • 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

    • 对于任意非空引用x,x.equals(null)应该返回false。

  • 完美equals方法建议

    • 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。

    • 检测this与otherObject是否引用同一个对象:

      这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。

    • 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。

    • 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:

      • 所有的子类都拥有统一的语义,就使用instanceof检测:
    • 将otherObject转换为相应的类类型变量:

    • 现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。

      • 如果在子类中重新定义equals,就要在其中包含调用super.equals(other)
  • 如果确定一个方法是覆盖方法,可以添加@Override告知编译器这个 方法要对超类的某个方法进行覆盖,如果没有找到对应的超类方法,则编译器报错

hashCode方法

  • 由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。

  • hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

  • Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。

  • 如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。

  • hashCode方法优化

    // 优化前
    public int hashCode() {
        return 7 * name.hashCode() // 可以使用null安全的Object.hashCode方法来改进
            + 11 * new Double(salary).hashCode() // 可以使用静态方法Double.hashCode()方法来避免创建Double对象
            + 13 * hireDay.hashCode(); // 可以使用null安全的Object.hashCode方法来改进
    }
    // 优化后
    public int hashCode() {
        return 7 * Object.hashCode(name)
            + 11 * Double.hashCode(salary)
            + 13 * Object.hashCode(hireDay);
    }
    // 还可以用hash()方法直接组合,hash()方法会对各参数分别调用Object.hashCode()方法,并组合这些散列值
    public int hashCode() {
        return Object.hash(name, salary, hireDay);
    }
    
    

toString方法

  • 在Object中还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。

  • 绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

    // Empoloyee类的toString方法
    public String toString() {
        return getClass().getName()
            + "[name=" + name
            + ",salary=" + salary
            + ",hireDay=" + hireDay
            + "]";
    }
    // Manager类的toString方法
    public String toString() {
        return super.toString() 
            + "[bonus=" + bonus
            + "]";
    }
    
  • 只要对象与一个字符串通过操作符“+”连接起来,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述

  • 在调用x.toString()的地方可以用""+x替代。这条语句将一个空串与x的字符串表示相连接。这里的x就是x.toString()。与toString不同的是,如果x是基本类型,这条语句照样能够执行。

  • 数组的toString方法要使用静态方法Array.toString,多维数组要使用静态方法Array.deepToString

  • 提示:强烈建议为自定义的每一个类增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅

泛型数组列表

  • ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如,ArrayList

  • 数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力:如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

  • 如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:

    staff.ensureCapacity(100);
    

    这个方法调用将分配一个包含100个对象的内部数组。然后调用100次add,而不用重新分配空间。(有点像C++的vector的reserve方法)

  • 一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
    一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用trimToSize。(有点像C++的vector的shrink_to_fit方法

  • C++注释:ArrayList类似于C++的vector模板。ArrayList与vector都是泛型类型。但是C++的vector模板为了便于访问元素重载了[]运算符。由于Java没有运算符重载,所以必须调用显式的方法。此外,C++向量是值拷贝。如果a和b是两个向量,赋值操作a=b将会构造一个与b长度相同的新向量a,并将所有的元素由b拷贝到a,而在Java中,这条赋值语句的操作结果是让a和b引用同一个数组列表。

  • java.util.ArrayList1.2

    • ArrayList()

      构造一个空数组列表。

    • ArrayList(int initialCapacity)

      用指定容量构造一个空数组列表。
      参数:initalCapacity 数组列表的最初容量

    • boolean add(E obj)

      在数组列表的尾端添加一个元素。永远返回true。
      参数:obj 添加的元素

    • int size()

      返回存储在数组列表中的当前元素数量。(这个值将小于或等于数组列表的容量。)

    • void ensureCapacity(int capacity)

      确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。
      参数:capacity 需要的存储容量

    • void trimToSize()

      将数组列表的存储容量削减到当前尺寸。

访问数组列表元素

  • java.util.ArrayList1.2
- `void set(int index,E obj)`

    设置数组列表指定位置的元素值,这个操作将覆盖这个位置的原有内容。
    参数:index 位置(必须介于0~size()-1之间)
    obj 新的值

- `E get(int index)`

    获得指定位置的元素值。
    参数:index 获得的元素位置(必须介于0~size()-1之间)

- `void add(int index,E obj)`

    向后移动元素,以便插入元素。
    参数:index 插入位置(必须介于0~size()-1之间)
    obj 新元素

- `E remove(int index)`

    删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。
    参数:index 被删除的元素位置(必须介于0~size()-1之间)
  • for each循环

    for (Employee e : staff) {
        do somthing;
    }
    

对象包装器与自动装箱

  • 有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。

  • 假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成ArrayList。这里就用到了Integer对象包装器类。我们可以声明一个Integer对象的数组列表。由于每个值分别包装在对象中,所以ArrayList的效率远远低于int[]数组。

    ArrayList list = new ArrayList<>();
    list.add(3); // 自动装箱(autoboxing),自动转变为:list.add(Integer.valueOf(3));
    int n = list.get(i); // 自动装箱:int n = list.get(i).intValue();
    Integer  n = 3;
    n++; //算数表达式也可以自动装箱
    
  • 包装器的==是检测对象是否指向同个区域,所以大概率是不相等的

  • 首先,由于包装器类引用可以为null,所以自动装箱有可能会抛出一个NullPointerException异常

  • 另外,如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double

  • 要想将字符串转换成整形,可以使用Integer类的静态方法

    String s = "sss";
    int x = Integer.parseInt(s);
    

参数数量可变的方法

  • 比如printf方法,这里的省略号...是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外

    public class PrintStream {
        public PrintStream print(String fmt, Object... args) { return format(fmt, args); }
    }
    
  • main方法甚至可以改成

    public static void main(String... args) {}
    

枚举类

  • public enum Size {SAMLL, MEDIUM, LARGE, EXTRA_LARGE};

  • 这个声明定义的类型是一个类,它刚好有4个实例,在此尽量不要构造新对象。因此,在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“==”就可以了。

  • 如果需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。

  • 所有的枚举类型都是Enum类的子类。它们继承了这个类的许多方法。其中最有用的一个是toString,这个方法能够返回枚举常量名。例如,Size.SMALL.toString()将返回字符串“SMALL”。

  • toString的逆方法是静态方法valueOf

    Size s = Enum.valueOf(Size.class, "SMALL"); // 返回指定名字、给定类的枚举常量。
    
  • 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。例如,如下调用:Size[] values = Size.value();,返回包含元素Size.SMALL,Size.MEDIUM,Size.LARGE和Size.EXTRA_LARGE的数组。

  • ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。例如:Size.MEDIUM.ordinal()返回1。

反射

  • 反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量地应用于JavaBeans中,它是Java组件的体系结构(有关JavaBeans的详细内容在卷Ⅱ中阐述)。特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。

  • 能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:

    • 在运行时分析类的能力。

    • 在运行时查看对象,例如,编写一个toString方法供所有类使用。

    • 实现通用的数组操作代码。

    • 利用Method对象,这个对象很像C++中的函数指针。

Class类

  • 在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。

  • 然而,可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class,这个名字很容易让人混淆。Object类中的getClass()方法将会返回一个Class类型的实例。

  • 如同用一个Employee对象表示一个特定的雇员属性一样,一个Class对象将表示一个特定类的属性。最常用的Class方法是getName()。这个方法将返回类的名字。如果类在一个包里,包的名字也作为类名的一部分

  • 还有静态方法forName获得类名对应的Class对象

    String className = "java.util.Random";
    Class c1 = Class.forName(className);
    
  • 获得Class类对象的第三种方法非常简单。如果T是任意的Java类型(或void关键字),T.class将代表匹配的类对象。例如:

    Class c1 = Random.class;
    Class c2 = int.class;
    Class c3 = Double[].class;
    

    请注意,一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。

  • 虚拟机为每个类型管理一个Class对象。因此,可以利用==运算符实现两个类对象比较的操作。还有一个很有用的方法newInstance(),可以用来动态地创建一个类的实例。例如,e.getClass().newInstance();创建了一个与e具有相同类类型的实例。newInstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。

  • 将forName与newInstance配合起来使用,可以根据存储在字符串中的类名创建一个对象。

    String s = "java.util.Random";
    Object m = Class.forName(s).newInstance();
    
  • C++注释:newInstance方法对应C++中虚拟构造器的习惯用法。然而,C++中的虚拟构造器不是一种语言特性,需要由专门的库支持。Class类与C++中的type_info类相似,getClass方法与C++中的typeid运算符等价。但Java中的Class比C++中的type_info的功能强。C++中的type_info只能以字符串的形式显示一个类型的名字,而不能创建那个类型的对象。

捕获异常

  • 当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个“捕获”异常的处理器(handler)对异常情况进行处理。

  • 如果没有提供处理器,程序就会终止,并在控制台上打印出一条信息,其中给出了异常的类型。可能在前面已经看到过一些异常报告,例如,偶然使用了null引用或者数组越界等。

  • 异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。

  • 如果类名不存在,则将跳过try块中的剩余代码,程序直接进入catch子句(这里,利用Throwable类的printStackTrace方法打印出栈的轨迹。Throwable是Exception类的超类)。如果try块中没有抛出任何异常,那么会跳过catch子句的处理器代码。

    try {
        String name = ...;
        Class c1 = Class.forName(name);
    } catch (Exception e) {
        e.printStackTrace();
    }
    
  • 对于已检查异常,只需要提供一个异常处理器。可以很容易地发现会抛出已检查异常的方法。如果调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误报告。

利用反射分析类的能力

  • 反射机制最重要的内容——检查类的结构

  • 在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。

package reflection;

import java.util.*;
import java.lang.reflect.*;

/**
 * This program uses reflection to print all features of a class.
 * @version 1.1 2004-02-21
 * @author Cay Horstmann
 */
public class ReflectionTest
{
   public static void main(String[] args)
   {
      // read class name from command line args or user input
      String name;
      if (args.length > 0) name = args[0];
      else
      {
         Scanner in = new Scanner(System.in);
         System.out.println("Enter class name (e.g. java.util.Date): ");
         name = in.next();
      }

      try
      {
         // print class name and superclass name (if != Object)
         Class cl = Class.forName(name);
         Class supercl = cl.getSuperclass();
         String modifiers = Modifier.toString(cl.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");
         System.out.print("class " + name);
         if (supercl != null && supercl != Object.class) System.out.print(" extends "
               + supercl.getName());

         System.out.print("\n{\n");
         printConstructors(cl);
         System.out.println();
         printMethods(cl);
         System.out.println();
         printFields(cl);
         System.out.println("}");
      }
      catch (ClassNotFoundException e)
      {
         e.printStackTrace();
      }
      System.exit(0);
   }

   /**
    * Prints all constructors of a class
    * @param cl a class
    */
   public static void printConstructors(Class cl)
   {
      Constructor[] constructors = cl.getDeclaredConstructors();

      for (Constructor c : constructors)
      {
         String name = c.getName();
         System.out.print("   ");
         String modifiers = Modifier.toString(c.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.print(name + "(");

         // print parameter types
         Class[] paramTypes = c.getParameterTypes();
         for (int j = 0; j < paramTypes.length; j++)
         {
            if (j > 0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
         }
         System.out.println(");");
      }
   }

   /**
    * Prints all methods of a class
    * @param cl a class
    */
   public static void printMethods(Class cl)
   {
      Method[] methods = cl.getDeclaredMethods();

      for (Method m : methods)
      {
         Class retType = m.getReturnType();
         String name = m.getName();

         System.out.print("   ");
         // print modifiers, return type and method name
         String modifiers = Modifier.toString(m.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.print(retType.getName() + " " + name + "(");

         // print parameter types
         Class[] paramTypes = m.getParameterTypes();
         for (int j = 0; j < paramTypes.length; j++)
         {
            if (j > 0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
         }
         System.out.println(");");
      }
   }

   /**
    * Prints all fields of a class
    * @param cl a class
    */
   public static void printFields(Class cl)
   {
      Field[] fields = cl.getDeclaredFields();

      for (Field f : fields)
      {
         Class type = f.getType();
         String name = f.getName();
         System.out.print("   ");
         String modifiers = Modifier.toString(f.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.println(type.getName() + " " + name + ";");
      }
   }
}

在运行时使用反射分析对象

  • 如果知道想要查看的域名和类型,查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。

  • 查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过getDeclaredFields得到的对象),obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。

    Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
    Class cl = harry.getClass();
    Field f = cl.getDeclaredFiled("name");
    Object v = f.get(harry); // the value of the "name" field of the object "harry", i.e., the String object "Harry Hacker",根据多态,一个变量(v)即可以引用超类对象,也可以引用子类的对象
    
  • 实际上,这段代码存在一个问题。由于name是一个私有域,所以get方法将会抛出一个IllegalAccessException。只有利用get方法才能得到可访问域的值。除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。

  • 反射机制的默认行为受限于Java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用Field、Method或Constructor对象的setAccessible方法。setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试、持久存储和相似机制提供的。

  • String域作为Object没问题,但是Java数值类型不是对象,这时反射机制会自动打包成Integer/Double等等,也可以使用Field类的getDouble方法

  • 刚才说明的是读域,有读就有写,用f.set(obj, value)可以修改域

  • 下面介绍一个可供任意类使用的通用toString方法。其中使用getDeclaredFileds获得所有的数据域,然后使用setAccessible将所有的域设置为可访问的。对于每个域,获得了名字和值。泛型toString方法需要解释几个复杂的问题。循环引用将有可能导致无限递归。因此,ObjectAnalyzer将记录已经被访问过的对象。另外,为了能够查看数组内部,需要采用一种不同的方式。有关这种方式的具体内容将在下一节中详细论述。

```java
// ObjectAnalyzer.java
package objectAnalyzer;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
public class ObjectAnalyzer
{
private ArrayList visited = new ArrayList<>();

/**
    * Converts an object to a string representation that lists all fields.
    * @param obj an object
    * @return a string with the object's class name and all field names and
    * values
    */
public String toString(Object obj)
{
    if (obj == null) return "null";
    if (visited.contains(obj)) return "...";
    visited.add(obj);
    Class cl = obj.getClass();
    if (cl == String.class) return (String) obj;
    if (cl.isArray())
    {
        String r = cl.getComponentType() + "[]{";
        for (int i = 0; i < Array.getLength(obj); i++)
        {
            if (i > 0) r += ",";
            Object val = Array.get(obj, i);
            if (cl.getComponentType().isPrimitive()) r += val;
            else r += toString(val);
        }
        return r + "}";
    }

    String r = cl.getName();
    // inspect the fields of this class and all superclasses
    do
    {
        r += "[";
        Field[] fields = cl.getDeclaredFields();
        AccessibleObject.setAccessible(fields, true);
        // get the names and values of all fields
        for (Field f : fields)
        {
            if (!Modifier.isStatic(f.getModifiers()))
            {
            if (!r.endsWith("[")) r += ",";
            r += f.getName() + "=";
            try
            {
                Class t = f.getType();
                Object val = f.get(obj);
                if (t.isPrimitive()) r += val;
                else r += toString(val);
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
            }
        }
        r += "]";
        cl = cl.getSuperclass();
    }
    while (cl != null);

    return r;
}
}
// ObjectAnalyzerTest.java
package objectAnalyzer;
import java.util.ArrayList;
/**
* This program uses reflection to spy on objects.
* @version 1.12 2012-01-26
* @author Cay Horstmann
*/
public class ObjectAnalyzerTest
{
public static void main(String[] args)
{
    ArrayList squares = new ArrayList<>();
    for (int i = 1; i <= 5; i++)
        squares.add(i * i);
    System.out.println(new ObjectAnalyzer().toString(squares));
}
}
```
 
 

使用反射编写泛型数组代码

  • 当一个Employee[]数组临时转换为Object[]数组是可行的,但是再也无法转回原来的Employee[]数组,当编写一个通用的copyOf方法时,得按照以下步骤考虑:

    • 首先获得a数组的类对象。Array类的静态方法

    • 确认它是一个数组。

    • 使用Class类(只能定义表示数组的类对象)的getComponentType方法确定数组对应的类型。

    public static Object goodCopyOf(Object a, int newLength) 
    {
        Class cl = a.getClass();
        if (!cl.isArray()) return null;
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
        return newArray;
    }
        ...
        String[] b = { "Tom", "Dick", "Harry" };
        b = (String[]) goodCopyOf(b, 10);
        System.out.println(Arrays.toString(b));
    

调用任意方法

  • 在C和C++中,可以从函数指针执行任意函数。从表面上看,Java没有提供方法指针,即将一个方法的存储地址传给另外一个方法,以便第二个方法能够随后调用它。事实上,Java的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为Java提供的接口(interface)(将在下一章讨论)是一种更好的解决方案。然而,反射机制允许你调用任意方法(相当于方法/函数指针)。

  • 在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名是:Object invoke(Object obj, Object... args)

    • 第一个参数是隐式参数,其余的对象提供了显式参数

    • 对于静态方法,第一个参数可以被忽略,即可以将它设置为null。

    • 例如,假设用ml代表Employee类的getName方法,下面这条语句显示了如何调用这个方法:String n = (String) m1.invoke(harry);

    • 如果返回类型是基本类型,invoke方法会返回其包装器类型。例如,假设m2表示Employee类的getSalary方法,那么返回的对象实际上是一个Double,必须相应地完成类型转换。可以使用自动拆箱将它转换为一个double:double s = (Double) m2.invoke(harry);

  • 如何得到Method对象呢?当然,可以通过调用getDeclareMethods方法,然后对返回的Method对象数组进行查找,直到发现想要的方法为止。也可以通过调用Class类中的getMethod方法得到想要的方法。Method getMethod(String name, Class... parameterTypes)

继承的设计技巧

  1. 将公共操作和域放在超类

    这就是为什么将姓名域放在Person类中,而没有将它放在Employee和Student类中的原因。

  2. 不要使用受保护的域

    然而,protected机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所有类都可以访问proteced域,而不管它是否为这个类的子类。

  3. 使用继承实现“is-a”关系

  1. 除非所有继承的方法都有意义,否则不要使用继承

  2. 在覆盖方法时,不要改变预期的行为

  3. 使用多态,而非类型信息

    使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。像下面这种代码完全可以用继承与多态来解决

    if (x is of type 1) action1(x);
    else if (x is of type 2) action2(x);
    
  4. 不要过多地使用反射

反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

你可能感兴趣的:(《Java核心技术》——读书笔记)