[关闭]
@Darcy 2017-08-11T11:17:42.000000Z 字数 11813 阅读 2148

第十章 更多Java构建块

JavaForKids

在前面的章节中,我们使用了大量的Java元素,还创建了一个Tic-Tac-Toe的游戏。但是我跳过了部分重要的Java元素,现在是时候来学习这些元素了。

1. 借助Date和Time编程

每台电脑都有一个内部时钟。每个Java程序都能在其中找到系统当前的日期和时间,并用不同的格式如06/15/2004或者是June 15, 2004来输出展示。尽管Java有非常多的类来处理日期和时间,但只要其中的两个: java.util.Datejava.text.SimpleDateFormat 就可以满足你操作日期和时间的大部分需求了。

创建一个对象来存储系统当前的日期和时间是很容易的:

  1. Date today = new Date();
  2. System.out.println( "The date is " + today );

上面程序的输出结果大致如下:

 The date is Fri Feb 27 07:18:51 EST 2004

SimpleDateFormat类让你可以用不同的格式来显示日期和时间。具体的做法是先用你所需的格式创建SimpleDateFormat类的实例,然后将Date对象作为其方法 format()的参数,并调用 format()。下面的程序采用了几种不同的格式来输入当前日期。

  1. import java.util.Date;
  2. import java.text.SimpleDateFormat;
  3. public class MyDateFormat {
  4. public static void main(String[] args) {
  5. // 创建一个日期对象, 输出它的默认格式
  6. Date today = new Date();
  7. System.out.println("The date is " + today);
  8. // 格式化输出日期,让它像02-27-04格式一样输出
  9. SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yy");
  10. String formattedDate = sdf.format(today);
  11. System.out.println("The date(dd-mm-yy) is " + formattedDate);
  12. // 格式化输出日期,让它像27-02-2004格式一样输出
  13. sdf = new SimpleDateFormat("dd-MM-yyyy");
  14. formattedDate = sdf.format(today);
  15. System.out.println("The date(dd-mm-yyyy) is " + formattedDate);
  16. // 输出日期格式为: Fri, Feb 27, ‘04
  17. sdf = new SimpleDateFormat("EEE, MMM d, ''yy");
  18. formattedDate = sdf.format(today);
  19. System.out.println("The date(EEE, MMM d, ''yy) is " + formattedDate);
  20. // 输出日期格式为: 07:18:51 AM
  21. sdf = new SimpleDateFormat("hh:mm:ss a");
  22. formattedDate = sdf.format(today);
  23. System.out.println("The time(hh:mm:ss a) is " + formattedDate);
  24. }
  25. }

编译并运行 MyDateFormat类,它将打印出如下内容:

The date is Fri Feb 27 07:34:41 EST 2004
The date(dd-mm-yy) is 02-27-04
The date(dd-mm-yyyy) is 27-02-2004
The date(EEE, MMM d, ''yy) is Fri, Feb 27, '04
The time(hh:mm:ss a) is 07:34:41 AM

Java的API文档的SimpleDateFormat部分介绍了更多的日期格式。你还能在文档中找到另一个类java.util.Calendar,它的很多方法都能用于处理日期。

2. 方法重载

方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数类型或参数个数。比如说,类System里面的println()方法就可以分别带有String,int,char或其他类型的参数来调用:

  1. System.out.println(“Hello”);
  2. System.out.println (250);
  3. System.out.println (‘A’);

尽管看起来似乎我们是调用了同一个方法println()三次,实际上,每次我们都是调用的不同的方法。你可能会疑惑为什么不直接创建不同名称的方法,比如printString(),printInt(),printChar()?原因之一是记住一个print的方法名要比记几个名字容易。使用方法重载还有其他的原因,解释起来会比较复杂,所以留给进阶型的书里讨论吧。

还记得我们在第四章提到的Fish类吗?这个类有个带有一个参数的dive()方法:

  1. public int dive(int howDeep)

我们来创建另一个不带有任何参数的dive()方法。这个方法会让鱼下潜5尺,除非当前深度已经超过了100尺。另外,这个新的Fish类还声明了一个新的final类型变量DEFAULT_DIVING,,它的值是5尺。

现在Fish类有两个重载方法dive()FishMaster类现在可以调用任意一个重载方法dive():

  1. public class FishMaster {
  2. public static void main(String[] args) {
  3. Fish myFish = new Fish(20);
  4. myFish.dive(2);
  5. myFish.dive(); // 一个新的重载方法
  6. myFish.dive(97);
  7. myFish.dive(3);
  8. myFish.sleep();
  9. }
  10. }

构造方法同样可以重载,但是创建对象时JVM只会选择一个参数列表匹配的来调用。 举个例子来说,如果你为Fish类添加一个不带参数的构造方法,FishMaster类可以使用下列任一方式来创建它的实例:

  1. Fish myFish = new Fish(20);

或者

  1. Fish myfish = new Fish();

3. 读取键盘输入

在这个部分你将学习程序如何在命令窗口输出问题,并懂得让用户从键盘去输入回应。这次我们将FishMaster传递给类Fish的硬编码(固定值)移除。现在程序将会提出问题 How Deep? ,鱼会根据用户的输入来决定下潜多少步。

你现在应该能很顺手地使用标准输入流System.out。顺便提一句,out变量是java.io.OutputStream的数据类型。现在我会向你解释如何使用标准输入流System.in,你应该也能猜到,in变量是java.io.InputStream的数据类型。

下一个版本的FishMaster类展示了在系统控制台等待用户输入的命令。在用户输入一个或更多的字符并按下回车键后,JVM将输入的字符放入InputStream流并传递给程序。

  1. import java.io.IOException;
  2. import java.io.BufferedReader;
  3. import java.io.InputStreamReader;
  4. public class FishMaster {
  5. public static void main(String[] args) {
  6. Fish myFish = new Fish(20);
  7. String feetString = "";
  8. int feets;
  9. // 创建InputStreamReader连接System.in,并把它传递给缓冲流
  10. BufferedReader stdin = new BufferedReader(new InputStreamReader(
  11. System.in));
  12. // 接着往下潜直到用户按下'Q'键盘
  13. while (true) {
  14. System.out.println("Ready to dive.How deep?");
  15. try {
  16. //程序暂停,等待用户输入一段文字,按回车之后结束
  17. feetString = stdin.readLine();
  18. if (feetString.equals("Q")) {
  19. // 退出程序
  20. System.out.println("Good bye!");
  21. System.exit(0);
  22. } else {
  23. // 把feetString转化成整数类型
  24. feets = Integer.parseInt(feetString);
  25. //鱼下潜的步数
  26. myFish.dive(feets);
  27. }
  28. } catch (IOException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. }

用户和FishMaster之间输出的对话大致如下:

Ready to dive.How deep?
14
Diving for 14 feet
I'm at 34 feet below the sea level
30
Diving for 30 feet
I'm at 64 feet below the sea level
Ready to dive.How deep?
Q
Good bye!

首先,FishMaster通过BufferedReader流与标准输入流System.in相关联,但是BufferedReader只能接受Reader类型的参数,而InputStreamReader是一个Reader类型的类,它可以把二进制的字节流转化成字符流,让我们可以按字符读出,你可以认为InputStreamReader是连接二进制流(InputStream)和字符流(Reader)的一个桥梁。接着程序输入一条信息 Ready to dive.How deep?readLine()方法让程序暂停直到用户按下回车键。由于输入的值是String类型,所以FishMaster将其转为整型并调用Fish中的dive()方法。这些操作一直循环直到用户按下字母Q来退出程序。feetString.equals(“Q”)是将String类型的变量feetString与字母Q比较。

刚才我们使用方法readLine()来一次获取用户输入的整行数据,你也可以用另一个方法System.in.read()用于处理用户输入的单个字符。

4. 关于java包

当程序员参与的是一个包含了大量类的项目中时,他们通常会将这些类放在不同的包中分类。比如某个包中可以包含所有展示界面的类,而另一个包中可以包含所有事件监听器相关的类,java.io就是提供给所有输入/输出类的包,javax.swing是图形工具操作类的包。

我们在Eclipse中创建一个新的项目PingPong。这个项目将类分别放在两个包中:screensengine。现在我们创建一个PingPongTable类, 将Package这行填上screens :

按下Finish键后Eclipse生成的代码中就会包含有包名。

  1. package screens;
  2. public class PingPongTable {
  3. public static void main(String[] args) {
  4. }
  5. }

顺便提一句,如果你的类中有一行带有关键字package,那么除了注释之外任何内容都不能写在这行上面。

每一个包都会存放在磁盘的不同文件夹中,Eclipse创建了文件夹screens并将文件PingPongTable.java放在里面。你可以到磁盘中查看到文件夹c:\eclipse\workspace\PingPong\screens,里面有PingPongTable.javaPingPongTable.class文件。


再来创建另一个PingPingEngine类并将engine作为包名。现在项目PingPong就有了两个包:

既然我们的两个类分别存放在不同的包(文件夹)中。那么PingPongTable类是无法直接访问到PingPongEngine类的,除非你添加import语句。

  1. package screens;
  2. import engine.PingPongEngine;
  3. public class PingPongTable {
  4. public static void main(String[] args) {
  5. PingPongEngine gameEngine = new PingPongEngine();
  6. }
  7. }

Java的包机制不仅能帮助你更好的管理类,还能严格控制外部包对类的访问权限。

5. 访问级别

Java中的类,方法和成员变量都可以指定它的访问级别为public, private, protected 或者 包级访问权限的其中一种。我们的类PingPongEngine使用public修饰,这意味着任意类都可以访问到它。我们来做个实验——声明 PingPongEngine时不带有关键字public。现在PingPongTable类甚至不能编译通过,会报PingPongEngine can not be resolved or is not a type(PingPongEngine 不能被转换或不是一个类型)的错误。这意味着PingPongTable无法再访问到PingPongEngine

如果某个类没有指定访问级别,那么它将具备包访问权限,即这个类仅对同一个包中的类可见。

同样的,如果你忘了给 PingPongEngine中的方法public访问级别,那么PingPongTable也会报出这些方法不可见的错误。下一章节中我们会写一个乒乓球游戏,让你能更好了解访问级别。


private访问修饰词用来向外部隐藏类的成员变量或方法。就像一辆汽车:大部分人不知道汽车外壳下面有什么,也压根不知道当司机踩下刹车的时候汽车内部是如何工作的。

看下面的程序代码。在Java中,我们可以说对象Car只向外部提供了一个public方法:brake(),而在brake()内部,则可能调用了其他一些我们不需要知道的方法。比如说,如果司机刹车踩的太厉害,那么汽车的电脑装置可能会提供特殊的防抱死机制。在前面我已经提到过,Java程序可以控制像火星探测器这样复杂的机器人,更不用说简单的汽车装置了。

  1. public class Car {
  2. // 这个私有的成员变量只能在这个类中使用
  3. private String brakesCondition;
  4. // 公开类型的brake()方法里面调用了私有的不同刹车方法
  5. public void brake(int pedalPressure) {
  6. boolean useRegularBrakes;
  7. useRegularBrakes = checkForAntiLockBrakes(pedalPressure);
  8. if (useRegularBrakes == true) {
  9. useRegularBrakes();
  10. } else {
  11. useAntiLockBrakes();
  12. }
  13. }
  14. //这个私有的方法只能在本类中被调用
  15. private boolean checkForAntiLockBrakes(int pressure) {
  16. if (pressure > 100) {
  17. return true;
  18. } else {
  19. return false;
  20. }
  21. }
  22. //这个私有的方法只能在本类中被调用
  23. private void useRegularBrakes() {
  24. //正常刹车的代码
  25. }
  26. //私有方法
  27. private void useAntiLockBrakes() {
  28. // 防抱死刹车
  29. }
  30. }

还有一个Java关键字protected来控制访问级别。如果你在方法声明时使用这个关键字,那么这个方法将只对当前类,当前类的子类以及同一个包中的类可见。其他包中的类都无法访问到这个方法。

面向对象语言的特性之一就是封装性,封装性可以用来隐藏或者保护类中的元素。

当你设计一个类时,隐藏的方法和成员变量对于外部是不可见的。如果汽车的设计者不把汽车外壳下的各种控制装置隐藏起来,那么司机将要处理成百的按钮,开关和计量表。

在下个部分,你将看到Score类将它的属性都用private方式声明。

6. 回到Arrays

在第九章,程序ScoreWriter创建了一个String类型的数组来存放在文件中玩家的名字和分数。
现在是时候来学习怎样用数组存放任意对象了。

这次我们将创建一个对象展示游戏分数,这个对象将包括运动员的姓名,得分,以及最后一次游戏时间。

Score类在下列代码片段中,它的每个属性都被声明为private,但都有gettersetter方法。当然,你 可能不太明白为什么Score对象不直接像下面那样为它的属性赋值:

  1. Score.score = 250;

而要使用

  1. Score.setScore(250);

试试打破常规来思考。如果稍后我们决定当玩家达到500的分数时,程序就会播放音乐。如果Score里面有方法setScore(),那么你需要修改的仅仅是这个方法, 直接在这个方法里面加入代码来检查分数决定是否需要播放音乐即可, 原来调用setScore()方法的类就不需要有任何的改动了。如果调用类直接改变属性值,音乐播放的切换就必须在调用类中实现。那当你需要在两个不同的游戏程序中使用Score的时候该怎么办呢?直接改变属性值,你需要在两个调用类中实现这些改变,但是如果本身就带有setter方法,这些操作就能被封装起来并很便捷地被每个调用类使用。

  1. import java.util.Date;
  2. public class Score {
  3. private String firstName;
  4. private String lastName;
  5. private int score;
  6. private Date playDate;
  7. public String getFirstName() {
  8. return firstName;
  9. }
  10. public void setFirstName(String firstName) {
  11. this.firstName = firstName;
  12. }
  13. public String getLastName() {
  14. return lastName;
  15. }
  16. public void setLastName(String lastName) {
  17. this.lastName = lastName;
  18. }
  19. public int getScore() {
  20. return score;
  21. }
  22. public void setScore(int score) {
  23. this.score = score;
  24. }
  25. public Date getPlayDate() {
  26. return playDate;
  27. }
  28. public void setPlayDate(Date playDate) {
  29. this.playDate = playDate;
  30. }
  31. // 拼接各种属性值,并且在最后加一个换行符。
  32. @Override
  33. public String toString() {
  34. String scoreString = firstName + " " + lastName + " " + score + " " +
  35. playDate + System.getProperty("line.separator");
  36. return scoreString;
  37. }
  38. }

程序ScoreWriter2将创建多个Score实例对象,并一 一为其赋值。

  1. import java.io.FileWriter;
  2. import java.io.BufferedWriter;
  3. import java.io.IOException;
  4. import java.util.Date;
  5. public class ScoreWriter2 {
  6. /**
  7. * 这个main方法执行了一下的动作:
  8. * 1. 创建了一个数组实例
  9. * 2. 创建各个Score对象并填入数组
  10. * 3. 把数组中的数据写入文件中
  11. */
  12. public static void main(String[] args) {
  13. FileWriter myFile = null;
  14. BufferedWriter buff = null;
  15. Date today = new Date();
  16. Score scores[] = new Score[3];
  17. // 玩家 #1
  18. scores[0] = new Score();
  19. scores[0].setFirstName("John");
  20. scores[0].setLastName("Smith");
  21. scores[0].setScore(250);
  22. scores[0].setPlayDate(today);
  23. // 玩家 #2
  24. scores[1] = new Score();
  25. scores[1].setFirstName("Anna");
  26. scores[1].setLastName("Lee");
  27. scores[1].setScore(300);
  28. scores[1].setPlayDate(today);
  29. // 玩家 #3
  30. scores[2] = new Score();
  31. scores[2].setFirstName("David");
  32. scores[2].setLastName("Dolittle");
  33. scores[2].setScore(190);
  34. scores[2].setPlayDate(today);
  35. try {
  36. myFile = new FileWriter("c:\\scores2.txt");
  37. buff = new BufferedWriter(myFile);
  38. for (int i = 0; i < scores.length; i++) {
  39. // 通过toString()方法来输出对象的有关信息,并输出到文件中
  40. buff.write(scores[i].toString());
  41. System.out.println("Writing " + scores[i].getLastName());
  42. }
  43. System.out.println("File writing is complete");
  44. } catch (IOException e) {
  45. e.printStackTrace();
  46. } finally {
  47. try {
  48. buff.flush();
  49. buff.close();
  50. myFile.close();
  51. } catch (IOException e1) {
  52. e1.printStackTrace();
  53. }
  54. }
  55. }
  56. }

如果一个程序试图访问超出数组长度的数组元素,比如:score[5].getLastName(),Java就会抛出ArrayIndexOutOfBoundsException的异常。

7. ArrayList类

ava.util包中包含了很多用来在内存中存放多个实例对象(集合)的类。 一些比较常用的类比如ArrayList, Vector, HashTable, HashMapList。我将向你展示怎么使用java.util.ArrayList

普通数组的缺点在于你必须得提前知道数组元素的数量。记得吗?为了创建一个数组实例,你必须得在中括号中放入数字来指定数组的容量。

  1. String[] myFriends = new String[5];

ArrayList类没有这个限制--在不知道这个集合需要存放多少对象的情况下你就能创建它的实例,需要的时候直接添加元素就可以了。
那为什么还要使用数组,一直用ArrayList就够了!不幸的是,没有事情是绝对完美的,你需要为ArrayList的便捷性付出代价:ArrayList的存取要比普通的数组慢,而且你只能在其中存入对象类型[1] ,比如你不能存放一组int型的数字在ArrayList中。

为了构建一个ArrayList对象,你首先需要进行实例化。创建你打算存放在ArraList中的对象实例,并通过调用ArrayList的方法add()来添加对象实例。下面这个简短的程序将一组String对象存放到一个ArrayList中并将这个集合打印出来。

  1. import java.util.ArrayList;
  2. public class ArrayListDemo {
  3. public static void main(String[] args) {
  4. // 创建一个存放String类型的ArrayList对象,并填充它
  5. ArrayList<String> friends = new ArrayList<>();
  6. friends.add("Mary");
  7. friends.add("Ann");
  8. friends.add("David");
  9. friends.add("Roy");
  10. // 朋友的数量
  11. int friendsCount = friends.size();
  12. // 输出ArrayList中的内容
  13. for (int i = 0; i < friendsCount; i++) {
  14. System.out.println("Friend #" + i + " is " + friends.get(i));
  15. }
  16. }
  17. }

这个程序将打印出下面几行:

Friend #0 is Mary
Friend #1 is Ann
Friend #2 is David
Friend #3 is Roy

请注意ArrayList<String>中的尖括号,它表示了在ArrayList里面只能存放String类型的对象,无论你通过add来添加对象,或是通过get来获取一个对象,它都是String类型的。当然,尖括号里面可以是任意的对象类型,你声明为哪种类型,ArrayList之后就和哪种类型绑定了,这种技术在编程中成为泛型技术。例如在下面的FishTank例子中,当你声明为ArrayList<Fish>之后,add方法和get方法ArrayList里面的其他方法,都只能接受Fish类型的对象了,如果你试图添加其他类型的对象,那将不能通过编译。

  1. import java.util.ArrayList;
  2. public class FishTank {
  3. public static void main(String[] args) {
  4. ArrayList<Fish> fishTank = new ArrayList<>();
  5. Fish aFish = new Fish(20);
  6. aFish.color = "Red";
  7. aFish.weight = 2;
  8. fishTank.add(aFish);
  9. aFish = new Fish(10);
  10. aFish.color = "Green";
  11. aFish.weight = 5;
  12. fishTank.add(aFish);
  13. //迭代输出fishTank中的内容
  14. for (Fish theFish : fishTank) {
  15. System.out.println("Got the " + theFish.color
  16. + " fish that weighs " + theFish.weight + " pounds. Depth:"
  17. + theFish.currentDepth);
  18. }
  19. }
  20. }

下面是程序FishTank的输出内容:

Got the Red fish that weighs 2.0 pounds. Depth:20
Got the Green fish that weighs 5.0 pounds. Depth:10

FishTank中我们看到一个新的for循环的语法结构,这是一种加强版的循环语法,你可以在数组或者各种集合中使用它来迭代输出容器里面的内容。

既然你已经了解过Java的访问权限, 那么, PetFish类可以做一些小修改了。age, color, weightheight这些成员变量可以声明为protected,而变量currentDepth应该声明为private。你应该添加public的方法比如getAge()来返回当前变量age的值,而setAge()用来设置age的值,其他变量同理。

编程习惯良好的程序员会为类添加方法来供其他类调用来修改变量,而不是让其他类直接修改它的成员变量。这就是为什么在前面的部分我们将Score类的变量声明为private,再提供settergetter方法来修改变量。

在这一章中我向你展示了不同的Java元素,这些元素看起来没有关联,但是专业的Java程序员都会经常用到这些元素。在完成这一章节的实践作业后,你应该能对这些元素如何共同使用有更好的理解。

8. 扩展阅读

1.Java 集合
http://t.cn/RLnvu06
2.ArrayList类
http://t.cn/RLnvEq1
3.Vector类
http://t.cn/RLnvefQ
4.Calendar类
http://t.cn/RLnP7Iv

9. 练习

1.为Fish类添加一个重载的没有参数的构造方法。这个构造方法要将初始位置设为10步。FishMaster类要创建一个如下所示的Fish对象的实例:

  1. Fish myFish = new Fish();

2.为Score类添加带有四个参数的构造方法。创建一个程序ScoreWriter3在创建Score对象实例时就给其属性赋值,而不是通过setter方法,比如:

  1. Score aScore = new Score("John", "Smith", 250, today);

10. 进一步的练习

在网上学习如何使用Vector类,尝试仿造ArrayListDemo创建一个VectorDemo程序。


創用 CC 授權條款
本著作係採用創用 CC 姓名標示-非商業性-禁止改作 2.5 中國大陸 授權條款授權.


[1] ArrayList只支持对象类型,而不支持基本类型,基本类型可以用其对应的对象类型来替换,比如int - Integer
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注