《Java核心技术》笔记
Nico Liao 阿蒙

《Java核心技术》是一套全面扎实的Java工具书,适合进阶学习,平时可以搁一本放身边随时查漏补缺,巩固基础。

[TOC]

第一章 java程序设计概述

1.1java的关键术语

  • 简单性
  • 面向对象
    • 定义:是一种程序设计技术,重点放在数据与对象的接口上。
    • 相比于c++,java有简单的接口概念以及元类模型
  • 网络技能
    • 有一个拓展的例程库用于处理TCP/IP,能通过URL打开访问网络上的对象
  • 健壮性
    • 与c++最大的不同在于其采用的指针模型可消除重写内存和损坏数据的可能性
  • 安全性
  • 可移植性
  • 解释性
  • 高性能
    • 字节码-机器码(即时编译)
  • 多线程
  • 动态性

题外话:HTML和XML
HTML的全称为超文本标记语言,是一种标记语言。它包括一系列标签.通过这些标签可以将网络上的文档格式统一,使分散的Internet资源连接为一个逻辑整体。HTML文本是由HTML命令组成的描述性文本,HTML命令可以说明文字,图形、动画、声音、表格、链接等。

HTML的缺点使其交互性差,语义模糊,这些缺陷难以适应Internet飞速发展的要求,因此一个标准、简洁、结构严谨以及可高度扩展的XML就产生了。

可扩展标记语言(XML)与Access,Oracle和SQL Server等数据库不同,数据库提供了更强有力的数据存储和分析能力,例如:数据索引、排序、查找、相关一致性等,XML的宗旨传输数据的,而与其同属标准通用标记语言的HTML主要用于显示数据。事实上XML与其他数据表现形式最大的不同是:他极其简单。这是一个看上去有点琐细的优点,但正是这点使XML与众不同。

第二章 java程序设计环境

小百科:
JDK 开发工具箱
JRE 运行java程序用户所使用的程序软件
SE 用于开发桌面或者简单服务器应用的Java平台
EE 用于复杂的服务器应用的Java平台
ME 用于手机和其他小型设备的Java平台
J2 用于命名老版的Java
SDK 用于命名老版的JDK
u 甲骨文公司的术语,用于发布修改的bug
NetBeans 甲骨文公司的集成开发环境

第三章 Java的基本程序设计结构

3.1 一个简单的Java应用程序

1
2
3
4
5
6
7
8
9
10
public class FirstSample
//public这个关键字称为访问修饰符(access modifier),用于控制程序的其他部分对这段代码的访问级别;class表明程序中的全部内容都包含在类之中
//class后面紧跟类名
{
public static void main(string[] args)//void表示这个方法没有返回值
{
System.out.println("Hello Swjtu")
}
}```

  1. ==Java对程序的大小写很敏感==
  2. 类是构建所有java应用程序和applet的构建块,后者的所有内容都应该放在类中
  3. 定义类名的规则如下:
    • 必须以大写字母开头,后面可跟字母数字任意组合
    • 长度没有限制
    • 不能使用Java保留字
    • 类名由多个单词组成时每个单词的首字母都应该大写(骆驼命名法),如CamelCase
  4. 源代码的文件名必须与公共类相同,并使用.java作为拓展名
  5. 编译运行时,源文件将从指定类中的main方法(函数)开始执行,故而源文件必须包含一个main方法,当然用户也可以自定义一个方法放在类中,并且在main中调用它
  6. 回车不是语句结束的标志,多条语句可以写在同一行之中。
  7. (.)号用于调用方法,Java的通用语句object.method(paraments);,这等价于函数调用
  8. ()内的叫做参数,在Java的方法之中可以没有参数,但需要空括号,例如
    System.out.println();

3.2 注释

  1. 注释的书写方式有三种:
    • 用“//”,其注释的内容到本行结尾
    • 可以用”/**/ “ 囊括一段注释,注意在java中这种注释方式不能嵌套,即如果代码本身包含了一个“*/”,就不能在两端将代码括起来
    • 第三种注释可以用来自动生成文档,以“/*”开始,以“/”结束

3.3 数据类型

  1. Java是一种强类型语言,意味着必须为每一个变量声明一种类型
  2. Java没有任何的符号类型
  3. 检测“非数值”的方法:

if(double.isNaN(x));

  1. 总共8种基本类型:4种整型+2种浮点型+1种用于表示Unicode编码的字符单元的字符类型+1种用于表示真值的boolean类型

  2. 整型(允许是负数)

    类型 储存需求/字节 取值范围
    int 4 约为正负二十亿
    short 2 -32768~32767
    long 8 约正负九后十六零
    byte 1 -128~127
    • long用于大数量,short和byte则用于特定场合

    • java整型的范围与机器无关,能解决很多跨平台的问

  1. 浮点型

    类型 储存需求 取值范围
    float 4 大约正负3.40282E+38F(有效位数6-7)
    double 8 大约正负1.79769E+308(有效位数15)
  • double的数值精度是float的两倍,绝大多数的应用程序都采用double类型
  • float的精度很难满足要求,用的少,例如在快速处理单精度数据或者需要储存大量的数据时才会采用
  • float类型后面有个F(例如3.14F),没有后缀F的浮点数值默认是double型
  • 所有的浮点类型的计算都遵循IEE 754规范,下面是用来表示溢出和出错情况的三个特殊的浮点数值:
    • 正无穷大
    • 负无穷大
    • NaN(不是一个数值,计算(0/0or负数的平方根)
  1. char型

    • char 类型原本用于表示单个字符,如今, 有些Unicode字符可以用一个char值描述, 另外一些Unicode 字符则需要两个char 值

    • char 类型的字面量值要用单引号括起来。例如:W 是编码值为65 所对应的字符常量。它与”A” 不同,”A” 是包含一个字符A 的字符串, char 类型的值可以表示为十六进制值,其范围从\u0000 到\Uffff。

    • 除了转义序列\u 之外, 还有一些用于表示特殊字符的转义序列

    • 我们强烈建议不要在程序中使用char 类型, 除非确实需要处理UTF-16 代码单元。最好将字符串作为抽象数据类型处理

  1. boolean型

    • boolean ( 布尔)类型有两个值: false 和true, 用来判定逻辑条件整型值和布尔值之间不能进行相互转换。

3.4 变量

  1. 变量名必须是一个以字母开头并由字母或数字构成的序列。与大多数程序设计语言相比,Java 中“ 字母” 和“ 数字” 的范围更大。字母包括 ’A’ ~ ’Z’、 ’a‘ ~ ’z’、‘_’ ,’$’或在某种语言中表示字母的任何 Unicode 字符。变量名中所有的字符都是有意义的,并且大小写敏感。变量名的长度基本上没有限制
  2. 不能使用 Java 保留字作为变量名
  3. 可以在一行中声明多个变量:
    int i, z; // both are integers
  4. 声明一个变量之后,必须用赋值语句对变量进行显式初始化, 千万不要使用未初始化的变量。
  5. 变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的程序编写风格。
  6. 习惯上,常量名使用全大写。
  7. 在 Java 中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为
    类常量。可以使用关键字 static fina丨设置一个类常量。 需要注意, 类常量的定义位于 main 方法的外部。 因此, 在同一个类的其他方法中也可以使用这个常量。 而且, 如果一个常量被声明为 public, 那么其他类的方法也可以使用这个常量。 在这个示例中,Constants2.CM就是这样一个常量
1
2
3
4
5
6
7
8
9
public class Consitants2
{
public static final double CM=2
public static void main(String[] args)
{double KE=3
System.out.println("RE="+CM*KE) ;
}

}

3.5 运算符

  1. 当参与 / 运算的两个操作数都是整数时, 表示整数除法;否则, 表示浮点除法。
  2. 整数的求余操作(有时称为取模)用 % 表示。 例如, 15/2 等于 7, 15%2 等于 1, 15.0/2 等于 7.5
  3. 整数被 0 除将会产生一个异常, 而浮点数被 0 除将会得到无穷大或 NaN 结果。
  4. 在 main 方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为strictfp(如public static strictfp void main(String[] args)), 这个类中的所有方法都要使用严格的浮点计算。
  5. 数学函数与常量
  • 想计算一个数值的平方根, 可以使用 sqrt 方法:

  • ```java
    double x = 4;
    double y = Math.sqrt(x);
    System.out.println(y); // prints 2.0

    1
    2
    3
    4
    5
    6
    7
    8

    - println 方法和 sqrt 方法存在微小的差异。println 方法处理 System.out 对象。但是,
    Math 类中的 sqrt 方法处理的不是对象,这样的方法被称为静态方法。

    - 在 Java 中,没有幂运算, 因此需要借助于 Math 类的 pow 方法。语句:

    - ```java
    double y = Math.pow(x, a);
  • floorMod 方法的目的是解决一个长期存在的有关整数余数的问题。floorMod(position + adjustment, 12),你总会得到一个在0-11之间的数值。

    • Math 类提供了一些常用的三角函数:
1
2
3
4
5
Math,sin
Math.cos
Math.tan
Math.atan
Math.atan
  • 指数函数以及它的反函数

    1
    2
    3
    Math.exp
    Math.log
    Math.loglO
  • pi 和 e 常量的近似值:

    1
    2
    Math.PI
    Math.E
  • 不必在数学方法名和常量名前添加前缀“ Math”, 只要在源文件的顶部加上下面这行代码就可以了。

    1
    2
    3
    import static java.1ang.Math.*;
    //例如:
    System.out.println("The square root of \u03C0 is " + sqrt(PI)) ;
  1. 数值类型之间的转换

image-20210831142638991

  • 在图 3-1 中有 6 个实心箭头,表示无信息丢失的转换; 有 3 个虚箭头, 表示可能有精度
    损失的转换。 例如,123 456 789 是一个大整数, 它所包含的位数比 float 类型所能够表达的
    位数多。 当将这个整型数值转换为 float 类型时, 将会得到同样大小的结果,但却失去了一定
    的精度。

  • 当使用上面两个数值进行二元操作时(例如 n + f, n 是整数, f 是浮点数,) 先要将两个操作数转换为同一种类型,然后再进行计算。

    • 如果两个操作数中有一个是 double 类型, 另一个操作数就会转换为 double 类型。
    • 否则, 如果其中一个操作数是 float 类型, 另一个操作数将会转换为 float 类型。
    • 否则, 如果其中一个操作数是 long 类型, 另一个操作数将会转换为 long 类型。
    • 否则, 两个操作数都将被转换为 int 类型。
  1. 强制类型转换
  • 强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。例如:

    1
    2
    double x = 9.997;
    int nx = (int) x;
  • 对浮点数进行舍人运算, 以便得到最接近的整数:

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

  • 可以在赋值中使用二元运算符;java也提供了自增、 自减运算符,关系运算;

8.==位运算符==

  • 处理整型类型时,可以直接对组成整型数值的各个位完成操作。这意味着可以使用掩码技术得到整数中的各个位。
1
& ("与") | ("或") , ^("异或"), ~ ("非")
  • 另外,还有>>和<< 运算符将位模式左移或右移。 需要建立位模式来完成位掩码时, 这两个运算符会很方便
1
int fourthBitFromRight = (n & (1<< 3)) >> 3;
  • 最后,>>>运算符会用 0 填充高位,这与>>不同,它会用符号位填充高位。不存在 <<<
    运算符。
  1. 括号与运算优先级
  • 与 C 或 C++ 不同,Java 不使用逗号运算符。 不过, 可以在 foi■ 语 句 的 第 1 和
    第 3 部分中使用逗号分隔表达式列表。

image-20210831145813401

image-20210831145844903

  1. 枚举类型:变量的取值只在一个有限的集合内。 变量的取值只在一个有限的集合内。

3.6 字符串

  • Java 没有内置的字符串类型, 而是在标准 Java 类库中提供了一个预定义类, 很自然地叫做 String。每个用双引号括起来的字符串都是 String类的一个实例
  1. 子串:String 类的 substring 方法可以从一个较大的字符串提取出一个子串。
1
2
3
4
String greeting = "Hello";
String s = greeting.substring(0, 3);
//加上下面这句,就能生成一个“Help!”
greeting = greeting.substring(0, 3) + "p!";
  1. 拼接:

    • 允许使用 + 号连接(拼接)两个字符串

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

    • 任何一个 Java 对象都可以转换成字符串

    • 如果需要把多个字符串放在一起, 用一个定界符分隔,可以使用静态 join 方法:

    • ```java
      String all = String.join(“ / “, “S”, “M”, “L”, “XL”);// all is the string “S / H / L / XL

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      3. 不可变字符

      - String 类没有提供用于修改字符串的方法
      - 不可变字符串却有一个优点:编译器可以让字符串共享。

      4. 检测字符串是否相等

      - `s.equals(t)`如果字符串 s 与字符串 t 相等, 则返回 true ; 否则, 返回 false。

      // 如何判断两个字符串是否相当
      // 输出为”The result is :true”

        String d="abc";
        String e="abc";
        boolean result = d.equals(e);
        System.out.println("The result is :" + result);
      
      1
      2
      3
      4
      5

      - s 与 t 可以是字符串变量, 也可以是字符串字面量,如下表达也是合法的:

      - ```java
      "Hello".equals(greeting)
    • 要想检测两个字符串是否相等,而不区分大小写, 可以使用 equalsIgnoreCase 方法。如下:

    • ```
      “Hello”.equalsIgnoreCase(“hel1o”)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32

      - 一定不要使用= 运算符检测两个字符串是否相等! 这个运算符只能够确定两个字串
      是否放置在同一个位置上。

      - 如果虚拟机始终将相同的字符串共享, 就可以使用= 运算符检测是否相等。但实际上只有字符串常量是共享的,而+ 或 substring 等操作产生的结果并不是共享的。

      5. 空 串 与 Null 串

      - 空串 "" 是长度为 0 的字符串。空串是一个 Java 对象, 有自己的串长度(0 ) 和内容(空)。不过,String 变量还可以存放一个特殊的值, 名为null, 这表示目前没有任何对象与该变量关联

      6. 码点和代码单元

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

      7. ==String API==

      - 每一个 API 的注释都以形如 java.lang.String 的类名开始。类名之后是一个或多个方法的名字,解释和参数描述。

      8. 阅读联机API文档

      - API文档是 JDK 的一部分, 它是HTML 格式的。 让浏览器指向安装 roK 的 docs/api/index.html 子目录, 就可以看到


      ### 3.7 输入输出

      1. 读取输入

      - 读取“ 标准输人流” System.in 就没有那么简单了。要想通 过控制台进行输人, 首先需要构造一个 Scanner 对象, 并与“ 标准输人流” System.in 关联。如下所示:

      - ```java
      Scanner in = new Scanner(System.in);
  • nextLine 方法将输入一行。

  • ```
    System.out.print(“What is your name? “);
    String name = in.nextLine();

    1
    2
    3
    4
    5
    6

    - 在这里, 使用 nextLine 方法是因为在输人行中有可能包含空格。 要想读取一个单词(以空白 符作为分隔符 ), 就调用:
    ```String firstName = in.nextO;```

    - 想要读取一个整数,就用:

    int age = in.nextlntO;
    System.out.print(“How old are you? “);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    - 与此类似, 要想读取下一个浮点数, 就调用 nextDouble 方法。

    - 最后, 在程序的最开始添加上一行:
    ```import java.util.*;```
    Scanner 类定义在 java.util 包中。 当使用的类不是定义在基本 java.lang 包中时, 一定要使用 import 指示字将相应的包加载进来。

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

    ```java
    Console cons = System.console();
    String username = cons.readLine("User name: ");
    cha「[] passwd = cons.readPassword("Password:");
  • 为了安全起见, 返回的密码存放在一维字符数组中, 而不是字符串中。 在对密码进 行处理之后, 应该马上用一个填充值覆盖数组元素,采用 Console 对象处理输入不如采用 Scanner 方便。 每次只能读取一行输入, 而没有 能够读取一个单词或一个数值的方法。

image-20210902090521956

  1. 格式化输出
  • 可以使用 SyStem.0Ut.print(x) 将数值 x 输出到控制台上。 这条命令将以 x 对应的数据类型 所允许的最大非 0 数字位数打印输出 X。 例如:
    double x = 10000.0 / 3.0;

    System.out.print(x);
    打印:3333.3333333333335
    而使用如下方式则可以用 8 个字符的宽度和小数点后两个字符的精度打印x:
    System.out.printf(%8.2f, x);

image-20210902091536575

image-20210902091751837

  • s 转换符格式化任意的对象,, 对于任意实现了 Formattable 接口的对象都
    将调用 formatTo 方法; 否则将调用 toString 方法, 它可以将对象转换为字符串。

  • printf方法中日期与时间的格式化选项。

    • System.out.printfCXtc", new DateO);

      这条语句将用下面的格式打印当前的日期和时间:

      Mon Feb 09 18:05:19 PST 2015

    • image-20210903193738764

    • image-20210903193831340

    某些格式只给出了指定 丨期的部分信息 t 。例如, 只有 FI 期 或 月 份 如果需要多次对口期操作才能实现对每一部分进行格式化的 Q 的就太笨拙了为此, 可以采用一个格式化的字符串指出要被格式化的参数索引。 索引必须紧跟在 % 后面, 并以 $ 终止。 如

    System.out.printf( "&l$s %2$tB %2$te, %2$tY", "Due date:", new DateQ());

    打印:Due date: February 9, 2015

    还可以选择使用 < 标志它指示前而格式说明中的参数将被再次使川也就是说, 下列
    语句将产生与前面语句同样的输出结果:

    System.out .printf ("%s %tB %<te, %<tY" , "Due date:" , new DateO) ;

  • 格式说明符的语法图。

    image-20210903194406508

  1. 文件的输入和输出
  • 要想对文件进行读取, 就需要一个用 File 对象构造一个 Scanner 对象, 如下所示:

    Scanner in = new Scanner(Paths.get("niyflle.txt") , "UTF-8") ;

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

  • 要想写入文件, 就需要构造一个 PrintWriter 对象。在构造器中,只需要提供文件名:

    PrintWriter out = new PrintWriter("myfile.txt", "UTF-8") ;

    如果文件不存在,创建该文件。 可以像输出到 System.out—样使用 print、 println 以及 printf
    命令。

    image-20210903200720534

    • 如果
      用一个不存在的文件构造一个 Scanner, 或者用一个不能被创建的文件名构造一个 PrintWriter,
      那么就会发生异常。Java 编译器认为这些异常比“ 被零除” 异常更严重。

    • 已经知道有可能出现“ 输人 / 输出” 异常。这需要在 main 方法中用 throws 子句标记, 如下所示:

      1
      2
      3
      public static void main(String口 args) throws IOException
      {
      Scanner in = new Scanner(Paths.get("myfi1e.txt"), "UTF-8") ;

      image-20210903201020643

3.8 控制流

  • Java没有goto语句,但break语句可以带标签,可以利用它实现从内层循环跳出的目的;还有一种变形的 for 循环, 在C或C++中没有这类循环。它有点类似于C#中的 foreach 循环。
  1. 块作用域
    • 块(即复合语句)是指由一对大括号括起来的若干条简单的 Java 语句。块确定了变量的作
      用域。一个块可以嵌套在另一个块中。
    • 不能在嵌套的两个块中声明同名的变量。
  2. 条件语句
  • if (condition) statement
  • if (condition) statementi else statementi
  • else 部分是可选的。else 子句与最邻近的 if 构成一组。
  1. 循环语句
  • while {condition ) statement
  • do statement while {condition );
  1. 确定循环
  • ```java
    for (int i = 1;i <= 10; i++)
    System.out.println(i);
    1
    2
    3
    4
    5
    6
    7
    8

    - 如果在 for 语句内部定义一个变量, 这个变量就不能在循环体之外使用。 因此, 如果希望在 for 循环体之外使用循环计数器的最终值, 就要确保这个变量在循环语句的前面且在外部声明!

    - 另一方面,可以在各自独立的不同 for 循环中定义同名的变量:

    ```java
    for (int i = 1; i <= 10; i++){..}
    for (int i = 11; i <= 20; i ++) {..}// OK to define another variable named i
  1. 多重选择:switch 语句
  • switch语句将从与选项值相匹配的 case 标签处开始执行直到遇到 break 语句,或者执行到switch i吾句的结束处为止。如果没有相匹配的 case 标签, 而有 default 子句, 就执行这个子句。

  • 如果你比我们更喜欢 switch 语句, 编译代码时可以考虑加上 -Xlint:fallthrough 选项,如下所示:

    这样一来, 如果某个分支最后缺少一个 break 语句, 编译器就会给出一个警告消息。

  • ```java
    javac -Xlint:fallthrough Test.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    - 如果你确实正是想使用这种“ 直通式”(fallthrough) 行为, 可以为其外围方法加一个标注@SuppressWamings("fallthrough")。 这样就不会对这个方法生成警告了 (标注是为编译器或处理 Java 源文件或类文件的工具提供信息的一种机制。 )

    - case 标签可以是:

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

    - 枚举常量。

    - 从 Java SE 7 开始, case 标签还可以是字符串字面量。例如:

    - ```java
    String input=...;
    switch (input.toLowerCaseO)
    {
    case "yes": // OK since Java SE 7
    ...
    break;
    ...}
    • 当在 switch 语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由 switch 的表
      达式值确定。例如:

    • ```java
      Size sz = . . .;
      switch (sz)
      {
      case SMALL: // no need to use Size.SMALL

      break;
      …}

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      5. 中断控制流程语句

      - 与用于退出 switch 语句的 break 语句一样, 它也可以用于退出循环语句。

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

      - ```java
      Scanner in = new Scanner(System.in);
      int n;
      read_data:
      while (. . .) // this loop statement is tagged with the label
      for (. . .) // this inner loop is not labeled
      {
      Systen.out.print("Enter a number >= 0: ");
      n = in.nextlntO;
      if (n < 0) // should never happen-can’t go on
      break read_data;
      ...// break out of readjata loop
      }}

    如果输入有误, 通过执行带标签的 break 跳转到带标签的语句块末尾。对于任何使用break语句的代码都需要检测循环是正常结束, 还是由 break 跳出。

  • 最后,还有一个 continue 语句。 与 break 语句一样, 它将中断正常的控制流程。continue语句将控制转移到最内层循环的首部。

  • ```java
    Scanner in = new Scanner(System.in) ;
    while (sum < goal )
    {
    System.out.print(“Enter a number: “);
    n = in.nextlntO;
    if (n < 0) continue;
    sum += n; // not executed if n < 0
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    如果 n<0, 则 continue语句越过了当前循环体的剩余部分, 立刻跳到循环首部。如果将 continue 语句用于 for 循环中, 就可以跳到 for 循环的“ 更新” 部分。 例如, 下面这个循环:

    - ```java
    for (count = 1; count <= 100; count++)
    {
    System.out.print("Enter a number, -1 to quit: ");
    n = in.nextlntO;
    if (n < 0) continue;
    sum += n; // not executed if n < 0
    }

    如果 n<0, 则 continue 语句跳到 count++ 语句。还有一种带标签的 continue 语句,将跳到与标签匹配的循环首部。

3.9 大数值

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

  • 使用静态的 valueOf 方法可以将普通的数值转换为大数值:

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

    1
    2
    Biglnteger c = a.add(b); // c = a + b
    Biglnteger d = c.multiply(b.add(BigInteger.valueOf(2))); // d = c * (b + 2)

3.10 数组

  • 在声明数组变量时,需要指出数组类型(数据元素类型紧跟 [] ) 和数组变量的名字。如

    int[] a = new int[100];//加上了初始化

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

    String[] names = new String[10] ;会创建一个包含 10 个字符串的数组, 所有字符串都为null。

  • 要想获得数组中的元素个数,可以使用 array.length。例如:

    1
    2
    for (int i = 0; i < a.length; i ++)
    System.out.println(a[i]);
  • For each 循环

    • Java 的一种功能很强的循环结构, 可以用来依次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标值而分心。

      for (variable : collection) statement

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

      1
      2
      for (int element : a)
      System.out.println(element):
  • 想打印数组中的所有值利用 Arrays 类的 toString 方法更简单。 调用 Arrays.toString(a), 返回一个包含数组元素的字符串, 这些元素被放置在括号内, 并用逗号分隔, 例如,“ [2,3,5,7,11,13]”、 代码如下:

    1
    System.out.println(Arrays.toString(a));
  • 数组初始化以及匿名数组

    • 在 Java中, 提供了一种创建数组对象并同时赋予初始值的简化书写形式。

      1
      int[] small Primes = { 2, 3, 5, 7, 11, 13 };

      甚至还可以初始化一个匿名的数组:

      1
      2
      new int[] { 17, 19, 23, 29, 31, 37 }
      small Primes = new int[] { 17, 19, 23, 29, 31, 37 };
  • 数组拷贝

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

      1
      2
      int[] luckyNumbers = smallPrimes;
      1uckyNumbers[5] = 12; // now smallPrimes[5] is also 12
    • 如果希望将一个数组的所有值拷贝到一个新的数组中去,就要使用 Arrays 类的 copyOf方法:

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

      第 2 个参数是新数组的长度。这个方法通常用来增加数组的大小:

      1
      luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);

      如果数组元素是数值型,那么多余的元素将被赋值为 0 ; 如果数组元素是布尔型, 则将赋值
      为 false。相反, 如果长度小于原始数组的长度,则只拷贝最前面的数据元素。

  • 命令行参数

    • 前面已经看到多个使用 Java 数组的示例。 每一个 Java 应用程序都有一个带 String arg[ ]参数的 main 方法。这个参数表明 main 方法将接收一个字符串数组, 也就是命令行参数.
  • 数组排序

    • 要想对数值型数组进行排序, 可以使用 Arrays 类中的 sort 方法:

      1
      2
      int[] a = new int[10000];
      Arrays.sort(a)
  • 多维数组

    • 在 Java 中, 声明一个二维数组相当简单。 例如:

      1
      double[][] balances;
    • 与一维数组一样, 在调用 new 对多维数组进行初始化之前不能使用它。 在这里可以这样初始化:

      1
      balances = new double[NYEARS] [NRATES]:

      另外, 如果知道数组元素, 就可以不调用 new, 而直接使用简化的书写形式对多维数组
      进行初始化。例如:

      1
      2
      3
      4
      5
      6
      7
      int[][] magicSquare =
      {
      {16, 3, 2, 13},
      {5, 10, 11, 8},
      (9, 6, 7, 12},
      {4, 15, 14, 1}
      };

      一旦数组被初始化, 就可以利用两个方括号访问每个元素, 例如,balances[i][j]

      例如使用初始余额来初始化这个数组的第一行:

      1
      2
      for (int j = 0; j < balances[0].length; j++)
      balances[0][j] = 10000;

      然后, 按照下列方式计算其他行:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      for (int i = 1; i < balances.length; i++)
      {
      for (int j = 0; j < balances[i].length; j++)
      {
      double oldBalance = balances[i - 1][j]:
      double interest = . . .;
      balances[i][j] = oldBalance + interest;
      }
      }
    • 要想快速地打印一个二维数组的数据元素列表, 可以调用:

      1
      System.out.println(Arrays.deepToString(a)) ;
    • for each 循环语句不能自动处理二维数组的每一个元素。它是按照行, 也就是一维教组处理的要想访问二维教组 a 的所有元素, 需要使用两个嵌套的循环, 如下所示:

      1
      2
      3
      for (double[] row : a)
      for (double value : row)
      do something with value
  • 不规则数组

    • Java 实际上没有多维数组, 只有一维数组。 多维数组被解释为“ 数组的数组。

    • 表达式balances[i]引用第 i 个子数组, 也就是二维表的第 i 行。它本身也是一个组,balances[i][j]引用这个数组的第 j 项。由于可以单独地存取数组的某一行, 所以可以让两行交换:

      1
      2
      3
      doubleQ temp = balances[i]:
      balances[i] = balances[i + 1];
      balances[i + 1] = temp;
    • 还可以方便地构造一个“ 不规则” 数组, 即数组的每一行有不同的长度。

      要想创建一个不规则的数组, 首先需要分配一个具有所含行数的数组。

      1
      int[][] odds = new int [NMAX + 1] [] ;

      接下来, 分配这些行。

      1
      2
      for (int n = 0; n <= NMAX ; n++)
      odds[n] = new int [n + 1] ;

第四章 对象与类

4.1 面对对象程序设计概述

  1. 面向对象的程序是由对象组成的, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
  2. 从根本上说, 只要对象能够满足要求, 就不必关心其功能的具体实现过程。在 OOP 中, 不必关心对象的具体实现, 只要能
    够满足用户的需求即可。
  3. 对于一些规模较小的问题, 将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。

image-20210905155918979

4.1.1类

  1. 类( class) 是构造对象的模板或蓝图。 由类构造 (construct) 对象的过程称为创建类的实例 (instance ) .
  2. 封装 ( encapsulation , 有时称为数据隐藏)是与对象有关的一个重要概念。
    • 从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。
    • 对象中的数据称为实例域( instance field ), 操纵数据的过程称为方法( method )。
    • 对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态 ( state )。
      无论何时, 只要向对象发送一个消息,它的状态就有可能发生改变。
    • 实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。
    • 程序仅通过对象的方法与对象数据进行交互。封装给对象赋予了“ 黑盒” 特征, 这是提高重用性和可靠性
      的关键。
  3. 有的类都源自于一个“ 神通广大的超类”,它就是 Object。
  4. 在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中, 只需提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来建立另外一个类的过程称为继承(inheritance)。

4.1.2对象

  1. 要想使用 OOP,—定要清楚对象的三个主要特性:

    • 对象的行为(behavior)—可以对对象施加哪些操作,或可以对对象施加哪些方法?

    • 对象的状态 (state)—当施加那些方法时,对象如何响应?

    • 对象标识(identity )—如何辨别具有相同行为与状态的不同对象?


  2. 同一个类的所有对象实例, 由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法定义的。

  3. 每个对象都保存着描述当前特征的信息。这就是对象的状态。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。 对象状态的改变必须通过调用方法实现(如果不经过方法调用就可以改变对象状态, 只能说明封装性遭到了破坏。)

  4. 作为一个类的实例, 每个对象的标识永远是不同的, 状态常常也存在着差异。

  5. 对象的这些关键特性在彼此之间相互影响着。


4.1.3 识别类

  1. 传统的过程化程序设计, 必须从顶部的 main 函数开始编写程序。在面向对象程序设计时没有所谓的“ 顶部”。对于学习OOP 的初学者来说常常会感觉无从下手。答案是: 首先从设计类开始,然后再往每个类中添加方法。

  2. 识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。

例如, 在订单处理系统中,有这样一些名词:
•商品(Item )
•订单(Order)
•送货地址(Shippingaddress)
•付 款 ( Payment )
•账户(Account)

这些名词很可能成为类 Item、 Order 等。接下来, 查看动词:商品被添加到订单中, 订单被发送或取消, 订单货款被支付。对于
每一个动词如:“ 添加”、“ 发送”、“ 取消” 以及“ 支付”, 都要标识出主要负责完成相应动作的对象。 例如, 当一个新的商品添加到订单中时, 那个订单对象就是被指定的对象, 因为它知道如何存储商品以及如何对商品进行排序。也就是说,add 应该是 Order 类的一个方法,而 Item 对象是一个参数。

4.1.4类与类之间的关系

  1. 最常见的关系有以下三种:

    • 依赖(“use-a”)

      依赖(dependence ), 即“ uses-a” 关系, 是一种最明显的、 最常见的关系。例如, Order类使用 Account 类是因为 Order 对象需要访问 Account 对象查看信用状态。但是 Item 类不依赖于 Account 类, 这是因为 Item 对象与客户账户无关。因此, 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。

      • 应该尽可能地将相互依赖的类减至最少。用软件工程的术语来说, 就是让类之间的耦合度最小。
    • 聚合(“has- a”)

      聚合(aggregation ), 即“ has-a” 关系, 又可称为关联,是一种具体且易于理解的关系。 例如, 一个Order 对象包含一些 Item 对象。聚合关系意味着类 A 的对象包含类 B 的对象。

    • 继承(“is-a”)

      继承(inheritance ), 即“ is-a” 关系, 是一种用于表示特殊与一般关系的。 例如, RushOrder类由 Order 类继承而来。 在具有特殊性的 RushOrder 类中包含了一些用于优先处理的特殊方法, 以及一个计算运费的不同方法;而其他的方法, 如添加商品、 生成账单等都是从Order 类继承来的。一般而言, 如果类 A 扩展类 B, 类 A 不但包含从类 B 继承的方法,还会拥有一些额外的功能。

4.2使用预定义类

  • 并不是所有的类都具有面向对象特征。 例如,Math 类。

程序中,可以使用 Math 类的方法, 如 Math.random, 并只需要知道方法名和参数 (如果有的话,) 而不必了解它的具体实现过程。这正是封装的关键所在,当然所有类都是这样。但遗憾的是, Math 类只封装了功能,它不需要也不必隐藏数据。 由于没有数据,因此也不必担心生成对象以及初始化实例域.

4.2.1对象和对象变量

  1. 在java之中,想要使用对象必须先构造对象,并指定其初始状态。

    • 使用构造器(construtor)构造新实例,构造器是一种特殊的方法,用来构造和初始化对象。

      在标准java库中包含了一个date类,它的作用是用来描述一个时间点:

      1
      december 31,1999,23:59:59 GMT 
  2. 构造器的名字应该与类名相同,因此,Date类的构造器的名字就叫做Date。想要构造一个Date对象,得在构造器前面加上一个new操作符,如下所示:

    1
    2
    new Date()
    //这个构造器构造了一个新的对象。这个对象被初始化成了当前的日期和时间
    • 也可将这个对象传递给一个方法:
    1
    System.out.println(new Date());
    • 还可以将一个方法应用于刚刚创建的额对象。Date类中就有一个string方法。这个方法将返回日期的字符串描述。如下:

      1
      String s = new Date.toString();

    在这两个例子里,对象仅仅使用了一次。通常我们希望对象可以多次使用,因此,可将对象放入一个变量之中。例如:

    1
    Date birthday = new Date();

    ==对象与对象变量之间的区别==

    • Date deadline;定义了一个对象deadline,它可以引用date类型的对象。但是,==一定要认识到,变量deadline不是一个变量,实际上也没有引用对象。此时不能把任何的Date方法应用在这个变量上。==例如下面这个语句:

      1
      s= deadline.toString();

      将产生编译错误。

      必须要先初始化变量deadline,这里有两个选择:

      1
      deadline = new Date();
      1
      deadline = birthday;

      image-20210905193827965

    • 在Java中,任何对象变量的值都是对储存在另一个地方的一个对象的引用。

    • 局部变量不会自动地初始化为null,而必须通过调用new或者将它们设置为null进行初始化。

4.2.2Java类库中的LocalDate类

  1. 类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。

  2. 不要使用构造器来构造 LocalDate 类的对象。实际上,应当使用静态工厂方法 (factory method) 代表你调用构造器。下面的表达式:

    1
    LocalDate.now()

    会构造一个新对象,表示构造这个对象时的日期。

    image-20210905195854572

4.2.3更改器方法与访问器方法

  1. 只 访 问 对 象 而 不 修 改 对 象 的 方 法 有 时 称 为 访 问 器 方 法

4.3用户自定义类

4.3.1 最简单的定义形式是:

1
2
3
4
5
6
7
8
9
10
11
12
class ClassName
{
field1
field2
...
construct1
construct2
...
method1
method2
...
}

例如下面这段代码所演示的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Employee
{
//instance field
private String name;
private double salary;
private Date hireDay;

//constrator
public Employee(String n,double s,int year,int month,int day)
{
name = n;
salary = s;
GregrorianCalendar calendar=new GregrorianCalendar(year,month-1,day);
hireday = calandar gettime();
}

//a method
public String getname()
{
return name;
}

//more method
.....

}

在一个源文件里只能有一个公共类,但是可以有任意数目的非公有类。

4.3.2 多个源文件的使用

  • 如果习惯将每一个类都放在一个单独的源文件之中,那么可以有两种编译源程序的方法:
    • 一种是使用通配符调用java编译器:javac Employee*.java,所有与通配符相匹配的源文件都将被编译成类文件。
    • 或者键入jacac.EmployeeTest.java

4.3.3剖析Employee类

1
2
3
4
5
public Employee(String n,double s,int year,int month,int day)
public String getName()
public double getSalary()
public Date getHireDay()
public void raiseSalary(double byPercent)

这个类的所有方法都被标记为public,这就意味着任何类的任何方法都能调用这些方法。接下来需要注意的是,Employee类的实例中有三个实例域用来存放将被操作的数据:

1
2
3
private String name;
private double salary;
private Date hireDay;

private能保证只有Employee类自身的方法才能访问这些实例域,而其他的方法则不能读写这些域。

对此相对比的是,极不推荐使用public来来标记实例域,因为这样做任何的方法都能读写修改该实例域中的数据,这也就破坏了封装。

4.3.4从构造器开始

1
2
3
4
5
6
7
public Employee(String n,double s,int year,int month,int day)
{
name=n;
salary=s;
GregrorianCalendar calendar = new GregroCalendar(year,month-1,day);
hireDay=calendar.getTime();
}
  • 可以看到,构造器类的名称相同。在构造Employee类的对象时,构造器会运行,以便将实例域初始化成所希望的状态。例如在这条代码new Employee("James Bond",10000,1950,1,1)创建Employee类实例时会把实例域设置成:
1
2
3
name = James Bond;
salary = 10000;
hireDay = january 1,1950;

构造器和其他方法的不同之处在于,它的调用总是伴随着new操作符的执行,而不能使用构造器对一个已经存在的对象进行操作来达到重新设置实例域的目的,否则的话会出现编译错误。

记忆要点

  1. 构造器与类同名;
  2. 每个类可以有一个以上的构造器;
  3. 构造器可以有0个,1个或者多个参数;
  4. 构造器没有返回值;
  5. 构造期总是伴随着new操作符一起使用

4.3.5隐式参数和显式参数

例如代码段:

1
2
3
4
5
public void raiseSalary(double byPercent)
{
double raise = salary*byPercent/100;
salary+=raise;
}

将调用这个方法的对象的salary的实例域设置成新值:

1
number007.raiseSalary(5);
  • 在方法名后面的括号里面的值就是一个显式参数
  • 出现在方法名前的Employee类对象就是隐式参数
  • 可以看到,显式参数是明显直白的出现在方法的声明之中的,而隐式参数没有出现在方法的声明之中;在每一个方法中,this表示隐式参数,例如可以把上述代码块的salary统统改成this.salary,可以明显地将实例域和局部变量区别开来。
  • 在java中,所有方法都必须在类的内部进行定义,但并不代表它们就是内联方法。

4.3.6 封装的优点

  • 有些时候需要获取或者设置实例域的值,应该提供下面三个内容:

    • 一个私有的数据域
    • 一个公有域的访问器方法
    • 一个公有域的更改器方法

    有以下两点好处:

    1. 可以改变内部实现,除了该类的方法之外不会影响其他代码;
    2. 更改器方法可以执行错误检查。
  • 注意不要编写返回引用可变对象的访问器方法。如果需要返回一个可变对象的引用,应该首先对它进行克隆,对象克隆是指存放在另外一个位置上的对象副本。

4.3.7基于类的访问权限

  • 一个方法可以访问同一个类下面所属的所有对象的数据,例如
1
2
3
4
5
6
7
8
class Employee
{
...
public boolean equals(Employee other)
{
return name.equals(other name);
}
}

4.3.8 私有方法

  • 有时我们可能希望一个计算机代码分成若干个独立的辅助方法,通常这些方法不应该成为公有接口的一部分,这是由于他们往往与当前的实现机制非常紧密,或者需要一个特别的歇息以及一个特别的调用次序。
  • 实现方法:只需要将关键字public改为private即可

4.3.9 final实例域

  • 可以将实例域定义成final,构建对象时必须初始化这样的域,且后续不能再对它进行修改。
  • final修饰符大都应用于基本类型域,或者不可变类的域。

4.4静态域和静态方法

4.4.1 静态域

  • 如果把域定义成static,每个类中只有一个这样的域,而每个一个对象对于所有的实例域却都有自己的拷贝。
1
2
3
4
5
public void setId()
{
id = nextId;
nextId++;
}

假设为Harry设定雇员标识码:

Harry.setId();

Harry的id域被设置为静态域nextId当前的值,并且静态域的值加1。

4.4.2 静态常量

  • 静态变量使用较少,静态常量使用的却比较多,例如PI:

    1
    2
    3
    4
    5
    6
    public class Math
    {
    ...
    public static final double PI = 3.14159265358979323846;
    ...
    }

    在程序中可以采用Math.PI的形式来获得这个常量。

  • 前面提到最好不要将域设置成public,但公有常量却没问题,因为out被设置为final,故不允许将其他打印流赋值给它。

4.4.3 静态方法

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

  • 可以认为静态方法是没有this参数的方法(在一个非静态的方法中,this参数表示这个方法的隐式参数)

  • Employee类的静态方法不能访问Id实例域,因为它不能操作对象。

  • 静态方法可以访问自身类中的静态域:

    1
    2
    3
    4
    5
    6
    public static int getNextId()
    {
    return nextId;
    }
    可以通过类名调用这个方法:
    int n = Employee.getNext();
  • 在下面两种情况下使用静态方法:

    • 一个方法不需要访问对象状态,其所需的参数都是通过显式参数提供的(例如:Employee.getNextID
    • 一个方法只需要访问类中的静态域(例如:Employee.getNextId)。

    4.4.4 工厂方法

    • 静态方法还有另外一种常见的用途,类似于LocalDate和NumberFormat的类使用静态工厂方法来构造对象。例如:
    1
    2
    3
    4
    5
    NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
    NumberFormat percentFormatter = NumberFormat.getpercentInstance();
    double x = 0.1;
    system.out.println(currencyFormatter.format(x));
    system.out.println(percentFormatter.format(x));

    为什么NumberFormat类不利用构造器来完成这些操作呢?主要有以下两个原因:

    • 无法命名构造器。构造器的名字必须与类名相同,但是,这里希望将得到的货币实例和百分比实例采用不同的名字。
    • 当使用构造器时,无法改变所构造对象的类型。而Factory方法将返回一个DecimalFormat类对象,这是NumberFormat的子类。

    4.4.5 main方法

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

    每一个类可以有一个main方法,这是一个常用于对类进行单元测试的技巧,例如可以在Employee类中添加一个main方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Employee{
    publlic employee(String n,double s,int year,int day)
    }{
    ...
    public static void main(String[] args )
    {
    Employee e = new Employee("ROmeo",50000,2003,3,31);
    e.raiseSalary(10);
    system.out.println(e.getName()+" "+e.getSalary());

    }
    ...
    }
    //如果想要独立地运行测试Employee类,只需要执行:`java Employee`
    //如果该类是一个更大型程序的一部分,就可以使用以下的语句来运行程序:
    //`java Application`
    //Employee类的main方法永远不会执行

4.5方法参数

回顾一下有关参数传递给方法的一些专业术语:

  1. 按值调用:表示方法接受的是调用者提供的值。
  2. 按引用调用:表示方法接受的是调用者提供的变量地址

一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。

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

  • 方法参数的两种类型:

    • 基本数据类型(数字,布尔值)
    • 对象引用
  • ==一个方法不可能修改一个基本数据类型的参数==,而对象引用作为参数则不一样了:

    1
    2
    3
    4
    public static void tripleSalary(Employee x)
    {
    x.raiseSalary(200);
    }

    ​ 当调用时:

    1
    2
    harry = new Employee(...);
    tripleSalary(harry);

    具体的执行过程为:

    1
    2
    3
    1. x被初始化为harry的拷贝,这里是一个对象的引用。
    2. raiseSalary方法引用与这个对象引用。x和harry同时引用的那个Employee对象的薪资提高了两倍。
    3. 方法结束之后,参数变量x不再使用。当然对象变量harry继续引用那个薪资增至三倍的固原对象。

    image-20210914162110845

image-20210914162402773

4.6对象构造

4.6.1 重载

  • 定义:有些类有多个构造器,例如可以构造一个空的StringBuilder对象:

    Stringbuilder messages = new Stringbuider();

    亦可指定一个初始字符串:

    StringBuilder todolist = new StringBuilder("To do:\n");

    这种特征就叫做重载,即如果多个方法有相同的名字,不同的参数,便产生了重载。

    编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。

    如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好,这个过程叫做重载解析。

    • Java允许重载任何方法,而不只是构造器方法。因此要完整地描述一个方法,需要指出方法名以及参数类型,这叫做方法的签名。例如String类有四个成为indexOf的公有方法,他们的签名是:

      1
      2
      3
      4
      indexOf(int)
      indexOf(int,int)
      indexOf(String)
      indexOf(String,int)

      返回类型不是方法签名的一部分,也就是说,不能有两个名字相同,参数类型也相同却返回不同类型值的方法。

    4.6.2 默认域初始化

    • 如果在构造器中没有显式地给域赋予初值,那么就会被自动得赋予初值:数值为0,布尔值为false,对象引用为null。

    这也是域和局部变量的主要不同点,必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的域,将会自动得初始化为默认值。

    • 最好在使用之前初始化域。

    4.6.3 无参数的构造器

    • 如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置成默认值。于是,实例域中的数值型数据设置成0,布尔型数据设置成false,所有对象变量设置成null。
    • 如果类中提供了至少一个构造器,但是没有提供无参数的构造器,则在构造对象时如果没有提供参数就会被视为不合法。

    4.6.4 显式域初始化

    • 通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。
    • 在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予给某个特定的实例域时,这种方式特别有用。
    • 初始值不一定是常量值,也可以调用方法对域进行初始化。

    4.6.5 参数名

    • 在编写很小的构造器时,常常在参数命名上出现错误。例如用单个字符,但这样只有阅读代码才能晓得参数的含义.

      1
      2
      3
      4
      5
      public Employee(String n,double s)
      {
      name=n;
      salary=s;
      }
    • 另外一个技巧就是在每个参数前面加上一个前缀“a”,例如:

      1
      2
      3
      4
      5
      public Employee(String aName,double aSalary)
      {
      name=aName;
      salary=aSalary;
      }
    • 还有一个常用的技巧,基于这样的现实:参数变量用同样的名字将实例域屏蔽起来。例如,将参数命名成salary,salary将引用这个参数,而不是实例域。但是可以采用this.salary的形式访问实例域。回想一下,this指示隐式参数,也就是所构造的对象。

    4.6.6 调用另外一个构造器

    • 关键字this引用方法的隐式参数,但它还有另外一个含义,如果构造器的第一个语句形如this(….),这个构造器将调用同一个类的另一个构造器,下面是个典型例子:

      1
      2
      3
      4
      5
      6
      public Employee(double s)
      {
      //calls Employee(String double)
      this("Employee #"+nextId s);
      nextId++;
      }

      当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String,double)。采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。

4.6.7 初始化块

  • 前文中有两种初始化数据域的方法:

    • 在构造器中设置值
    • 在声明中赋值

    实际上还存在第三种机制,成为“初始化块”。在一个类的声明中,可以包含多个代码块,只要构造类的对象,这些块就会被执行。

  • 由于初始化数据域有多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:

    1. 所有数据域被初始化为默认值
    2. 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块
    3. 如果构造器第一行调用量第二个构造器,则执行第二个构造器主体
    4. 执行这个构造器的主体

    编写代码时我们应当精心组织好初始化代码,有利于其他人理解。

  • 如果对类的静态域进行初始化的代码比较复杂,你们可以使用静态的初始化块。将代码放在一个块中,并标记关键字static。

4.6.8 对象析构与finalize方法

  • Java中有自动的垃圾回收器,不需要人工回收内存,故而Java不支持析构器

析构器:计算机语言中的析构函数,当一个对象在消亡的时候,由编译器自动调用,主要用来释放内存。

4.7 包

  • Java允许使用包将类组织起来,借助于宝可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
  • 标准的Java类库分布在多个包中,包括java.lang,java.util和java.net等。
  • 标准的java包具有一个层次结构,也可以使用嵌套层次组织包。所有标准的java包都处在java和javax包层次中。
  • 使用包的主要原因是确保类名的唯一性,同名的类放在不同的包中,不会产生冲突;为了保证包名的绝对唯一性,建议将域名以逆序的形式作为包名,如com.horstman,还能进一步地划分为子包,如com.horstman.corejava
  • 从编译器的角度来看,嵌套的包没有任何关系。

4.7.1 类的导入

  • 一个类可以使用所属包的所有类,以及其他包中的共有类。采用两种方式访问另一个包中的公有类:

    • 第一种方式是在每个类名之前添加完整的包名。较为的繁琐,例如

      1
      java.time.LocalDate today = java.time.LocalDate.now();
    • 简单常用的另外一种是使用import语句,import语句是一种引用包含在包中的类的简明描述。一旦使用了import语句,在使用类时,就不必写出包的全名了。

    • 可以使用import语句导入一个特定的类或者整个包。该语句应该位于源文件的顶部。

    • 在多数情况下,只导入需要的包,不必过多得注意它们,但是如果发生命名冲突时,就需要注意他们的名字了。

      例如,java.utiljava.sql中都有日期(Date)类,导入 如下两个包时:

      1
      2
      import java.util.*;
      import java.sql.*;

      当程序在使用Date类时,编译就会出现以下错误:

      1
      Date today; // Error java.util.Date or java.sql.Date?

      因为此时编译器无法确定是使用哪个Date类,此时应该在导入处写明具体是哪个包中的Date类;如果都需要使用时那么在使用处应该标明完整的包名。

4.7.2 静态导入

  • import不仅可以导入类,还可以导入静态方法和静态域

    例如,在源文件的顶部,增加一条这样的指令:import static java lang.system.*;

    就可以使用system的静态方法和静态域的时候,不加类名前缀:

    out.println("Hello World");

    exit(0);

  • 这种方式可以让代码美观:

    1
    2
    sqrt(pow(x,2)+pow(y,2));//美观
    Math.sqrt(Math.pow(x,2)+Math.pow(y,2));//繁杂

4.7.3 将类放入包中

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

    1
    2
    3
    4
    5
    package com.horstman.corejava;
    public class Employee
    {
    ...
    }
  • 如果没有在源文件中放置package语句,这个源文件中的类会被放置在一个默认的包中(defaulf package)中。

4.7.4 包作用域

  • 包不是一个封装的实体。也就是说,任何人都可以向包中添加更多的类。

4.8类路径

  • 类储存文件系统的子目录中,类的路径必须与包名匹配,另外,类文件也可以储存在JAR(java归档)文件中。

  • 为了使类能够被多个程序共享,需要一下几点:

    • 把类放在一个目录中,这个目录是包树状结构的基目录。
    • 将JAR文件放在一个目录中
    • 设置类路径(class path)

    在UNIX环境中,类路径的不同项目之间采用冒号(;)分隔;而在Windows系统中,则以分号(;)分隔。

  • 类路径所列出的目录和归档文件是搜寻类的起始点。

  • 编译器定位文件比虚拟机复杂的多。如果引用一个类,而没有指出这个类所爱的包,那么编译器将首先查找包含着这个类的包,并查询所有的import指令,确定其中是否包含被引用的类。

4.8.1 设置类路径

  • 最好采用-classpath(或者-cp)选项指定类路径。
  • 整个指令应该书写在一行中。将这样一个长的命令行放在一个shell脚本或者一个批处理文件中是一个不错的主意。
  • 利用 -dasspath 选项设置类路径是首选的方法, 也可以通过设置 CLASSPATH 环境变量 完成这个操作。

4.9文档注释

  • 如果在源代码中添加以专用的定界符 /** 开始的注释, 那么可以很容易地生成一个看上 去具有专业水准的文档。这是一种很好的方式, 因为这种方式可以将代码与注释保存在一个 地方。如果将文档存人一个独立的文件中, 就有可能会随着时间的推移, 出现代码和注释不 一致的问题。 然而, 由于文档注释与源代码在同一个文件中, 在修改源代码的同时, 重新运 行 javadoc 就可以轻而易举地保持两者的一致性。

4.9.1 注释的插入

  • 应该编写注释的部分:
    • 共有类与接口
    • 共有的和受保护的构造器以及方法
    • 共有的和受保护的域
  • 注释应该放置在所描述的特性前面,每个/***…..*/文档注释在标记之后紧跟着“自由格式文本”,标记以@开始,如@author,@param
  • 自由格式文本的第一句应该是一个概要性的句子,javadoc实用程序将自动得将这些句子抽取出来形成概要页。
  • 在自由格式文本中,可以使用HTML修饰符。

4.9.2 类注释

  • 类注释必须放在import语句之后,类定义之前。

4.9.3 方法注释

  • 每一个方法注释必须放在所描述方法之前。除了通用的标记之外,还可以使用下面的标记:

    • @param 变量描述

      这个标记将对当前方法的“ param” (参数)部分添加一个条目。这个描述可以占据多

      行, 并可以使用 HTML 标记。一个方法的所有 @param 标记必须放在一起。

    • return 描述

      这个标记将对当前方法添加“ return” (返回)部分。这个描述可以跨越多行, 并可以

      使用 HTML 标记。

    • throws类描述

      这个标记将添加一个注释, 用于表示这个方法有可能抛出异常。

4.9.4 域注释

  • 只需要对公有域(通常是指静态变量)建立文档。

4.9.5 通用注释

下面的标记可以用在类文档的注释中:

  • eauthor 姓名

    这个标记将产生一个 ** author” ( 作者)条目。可以使用多个 @aUthor 标记, 每个 @

    author 标记对应一个作者

  • ©version

    这个标记将产生一个“ version ”(版本)条目。 这里的文本可以是对当前版本的任何描

    述。

下面的标记可以用于所有的文档注释中:

4.9.6 包域概述注释

  • 要想产生包注释, 就需要在每一个包目录中添加一个单独的文件。可以 有如下两个选择:
    • 提供一个以 package.html 命名的 HTML 文件。在标记 … 之间的所有 文本都会被抽取出来。
    • 提供一个以 package-info.java 命名的 Java 文件。 这个文件必须包含一个初始的以 /** 和 */ 界定的 Javadoc 注释, 跟随在一个包语句之后。它不应该包含更多的代码或注释。
    • 还可以为所有的源文件提供一个概述性的注释。 这个注释将被放置在一个名为 overview, html 的文件中, 这个文件位于包含所有源文件的父目录中。标记 … 2间的所 有文本将被抽取出来。 当用户从导航栏中选择“ Overview ” 时, 就会显示出这些注释内容。

4.9.7 注释的抽取

  • 可以使用多种形式的命令行选项对 javadoc 程序进行调整。
  • -version 选项在文档中包含 @author 和 @version 标记 (默认情况下, 这些标记会被省 略)。另一个很有用的选项是-link, 用来为标准类添加超链接。

4.10类设计技巧

使设计出来的类更具有 OOP 的专业水准的技巧:

  1. 一定要保证数据私有

    绝对不要破坏封装性,数据的表现形式很可能会发生改变,但是它们的使用方式不会经常改变。

  2. 一定要对数据初始化

    java不会对局部变量进行初始化,但会对对象的实例域进行初始化,最好不要依赖于系统的默认值,应该显式地初始化所有的数据。

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

    用其他的类代替多个相关的基本类型的使用。

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

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

  6. 类名和方法名要能够体现它们的职责

  7. 优先使用不可变的类

第五章 继承

5.1 类,超类和子类

  • “is-a”关系是继承的一个明显特征
  • 在Java中,所有继承都是公有继承,没有c++中的私有继承和保护继承

5.1.1 定义子类

  • 关键字extends表示继承

    下面由继承Employee类来定义Manager类的格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Manager Employee
    {
    ...//添加方法与域,例如:
    private double bonus;
    ...
    public void setbonus(double bonus )
    {
    this.bonus = bonus;
    }
    }

    image-20210923103914943

  • 再通过拓展定义子类的时候,仅需要指出子类和超类的不同指出。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。

5.1.2 覆盖方法

  • ```java
    public double getSalary()
    {
    return salary + bonus;//不能工作
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    ==问题在于Managerer类的getSalary方法不能直接地访问超类的私有域,也就是说尽管每个Manager对象都有一个名为salary的域,但在Manager类的getSalary方法中并不能够直接地访问salary域,只有Employee类的方法才能够访问私有部分, 如果 Manager 类的方法一定要访问私有域, 就必须借助于公有的接口, Employee 类中的 公有方法 getSalary 正是这样一个接口。

    - 在子类中可以增加域,增加方法或者覆盖类的方法,然而绝对不能删除继承的任何域和方法.

    #### 5.1.3 子类构造器

    ```java
    public Manager(String name,double salary,int year,int day)
    {
    super (name,salary,year,month,day);
    bonus = 0;
    }

这里的关键字super有着不一样的含义,语句super(n,s,year,month,day)是”调用超类Employee中含有n,s,year,month和day参数的构造器“的简写模式。

  • 使用super调用构造器的语句必须是子类构造器的第一条语句。

this关键字的两个用途:

  • 一是引用隐式参数
  • 二是带哦用该类的其他构造器

对比之下,super也有两个用途:

  • 一是调用超类的方法
  • 二是调用超类的构造器

注意点:在调用构造器的时候, 这两个关键字的使用方式很相似。调用构造器的语句只能作为另 一个构造器的第一条语句出现。构造参数既可以传递给本类(this) 的其他构造器,也可 以传递给超类(super) 的构造器。

  • 一个对象变量可以指示多种实际类型的现象被称为==多态==
  • 在运行时能够自动地选择调用哪个方法的现象被称为动态绑定。

5.1.4 继承层次

  • 继承并不只限于一个层次
  • 由一个公共超类派生出来的所有类的集合被称为继承层次
  • 在继承层次中,从某个特定的类到其祖先的路径被称为该累的继承链
  • 一个祖先类可以拥有多个子孙继承链
  • java不支持多继承

image-20210925190015571

5.1.5 多态

  • 判断是否应该设计成继承关系的简单规则,就是”is-a“guize,它表明子类的每个对象也是超类的对象。例如,每个经理都是雇员。

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

    例如,将一个子类对象赋值给超类对象

    1
    2
    3
    Employee e:
    e = new Employee(...);
    e = new Manager(...);
  • 对象变量是多态的,存在一个变量即可以同时引用父子类的对象

  • 在JAVA中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换

5.1.6 理解方法调用

  • 调用过程的详细描述

    • 编译器查看对象的声明类型和方法名

    • 编译器将查看调用方法时提供的参数类型

      调用x.f(param),在列举完类中所有名为f 的方法之后,其中存在一个与提供的参数类型完全匹配的,就选择调用这个方法,这个过程被称为重载解析。

    • 如果没有找到一个与参数类型相匹配的方法,或者发现经过类型转换之后有多个方法与只匹配,就会报错。

    • 至此,编译器已获得需要调用的方法名字和参数类型

  • 如果是private方法,static方法,final方法或者构造器,那么编译器将可以准确的知道该调用哪个方法,我们将这种调用方式成为静态绑定。

  • 当程序运行时,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。

  • 动态绑定由一个非常重要的特性,就是无需对现存的代码进行修改,就可以对程序进行拓展。

    image-20210926145056863

7.1.7 阻止继承:final类和方法

  • 不允许拓展的类被称为final类。例如,人民希望阻止定义Excutive类的子类,就可以在定义这个类的时候使用final修饰符声明。
  • 将方法和类声明为final的主要目的是:确保他们不会在子类中改变语义。

5.2 所有类的超类

5.3 泛型数组列表

5.4 对象包装器与自动装箱

5.5 参数数量可变的方法

5.6 枚举类

5.7 反射

5.8 继承的设计技巧

第六章 接口,lambda表达式与内部类

待更……

  • 本文标题:《Java核心技术》笔记
  • 本文作者:Nico Liao
  • 创建时间:2021-11-23 20:16:59
  • 本文链接:https://www.lzp.zone/2021/11/23/《Java核心技术》笔记/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论