分享

Java编程入门(2.3):类、对象和子程序

 yy99k 2017-09-14

2.3.1 内置子程序和函数

回想一下,子程序是组织在一起给定名称的一组程序指令。子程序用来执行一些任务。为了执行程序中的任务,可以使用子程序调用语句来“调用”子程序。在第四章中将会学到怎样编写自己的子程序,但直接调用语言内部已经写好的子程序就能完成很多事情。Java中,每个子程序都位于类或对象中。读者可以使用一些类,这些类是Java语言标准的一部分,其中包含了预定义的子程序。字符串类型的值是一个对象,其中含有可用于操作字符串的子程序。这些子程序都“内置”在Java语言中。哪怕不知道其内部实现方式,也可以调用这些字符串类型的子程序。事实上,这就是子程序的核心特点:子程序是一个黑盒,不需要掌握其内部原理就可以使用。

让我们首先介绍类组成部分之一的子程序。类的目的之一是将一些变量和子程序组合起来。这些变量和子程序称为类静态成员。前面见过的一个例子:在程序中有一个类,main()函数是类的静态成员。在类定义的部分,静态成员的定义都标有保留字“static”,如public static void main..中的“static”。

当一个类包含静态变量或者子程序时,类的名称是这个变量或者子程序全名的一部分。例如:被命名为System的标准类包含一个子程序exit,为了在程序中使用这个子程序,就必须使用System.exit。这个全名包括类的名称,后跟一个句号,接着是子例程的名称。这个子程序需要一个整数作为参数,所以实际上使用子例程调用语句如下:

1
System.exit(0);

调用System.exit将终止程序并关闭Java虚拟机。在结束主进程之前,如果有某种原因要终止程序就可以调用这个语句。(参数是告诉计算机程序终止的原因,参数值0表示程序正常结束,其它任何值表示程序终止是因为发现了错误,因此可以调用System.exit(1)来表示程序检测到错误后终止。参数被发传回操作系统,在实际中,操作系统通常会忽略这个值。)

System只是Java中许多标准类中的一个,另一个有用的类称为Math。这个类是一个静态变量的例子:包括变量Math.PI和Math.E,它们的值是数学常量π和e。Math还包括大量的数学“函数”,每个子程序都执行一些特定的任务。有些子程序的任务是计算或检索数据值。这种类型的子程序称为函数,函数会有一个返回值。一般来说,函数的调用者会以某种方式使用这个返回值。

读者应该熟悉计算数的平方根这个数学函数,Java中对应的函数为Math.sqrt。这个函数是一个名为Math类的静态成员子程序。如果x是一个数值,那么Math.sqrt(x)计算并返回该值的平方根。因为Math.sqrt(x)代表一个值,在子程序调用语句中把它单独放在一行并没有意义,如:

1
Math.sqrt(x);   // This doesn't make sense! 这是没有意义的!

在这种情况下,计算机会对函数计算出来的值进行处理吗?必须告诉计算机用这个值做些什么,比如让计算机把计算结果显示出来:

1
System.out.print( Math.sqrt(x) );  // Display the square root of x. 显示x的平方根

或者可以用一个赋值语句告诉计算机将该值存储在一个变量中:

1
lengthOfSide = Math.sqrt(x);

调用函数Math.sqrt(x)代表返回一个双精度类型的值,可用于任何用到双精度类型的数值字面值的地方。

Math类包含许多静态成员函数,这里列举了一些比较重要的函数:

  • Math.abs(x)计算x的绝对值。
  • 常用的三角函数Math.sin(x)、Math.cos(x),和Math.tan(x)。(所有的三角函数计算角度都使用弧度 radian,不用度degree。)
  • 反三角函数arcsin、arccos和arctan写成:Math.asin(x)、Math.acos(x)和Math.atan(x),返回值用弧度表示,不用度。
  • 指数函数Math.exp(x)计算e的x次幂,自然对数函数Math.log(x)计算以e为底的x的对数。
  • Math.pow(x,y)计算x的y次幂。
  • Math.floor(x)四舍五入,向下取小于或等于x的最大整数。尽管返回值是数学形式的整数,但是返回的其实是一个双精度类型的值,而不是与读者期望的一样是整型。例如,Math.floor(3.76)等于3.0。函数Math.round(x)是返回与x最近的整数,Math.ceil(x)四舍五入,向上取大于x的最小整数。(”Ceil”是”ceiling”的简写,与”floor”相反)
  • Math.random()在0.0 <= Math.random() < 1.0范围内返回一个随机选择的双精度值。(计算机实际上计算的是所谓的“伪随机”数字。它们不是真正的随机数,但是有足够的随机性,能满足大多数要求。)读者将会在后面发现许多Math.random的使用示例。

以上这些函数,参数(括号内的x或者y)的类型可以是任何数字类型的任何值。对于大部分函数来说,不管它的参数是什么类型,函数返回的值都是双精度类型。但Math.abs(x)的返回值与x的类型一样,如果x是整型,那么Math.abs(x)也是整型。因此,当Math.sqrt(9)是双精度类型值3.0时,Math.abs(9)是整型值9。

注意Math.random()没有任何参数,但仍然需要括号,即使括号里什么都没有。括号让计算机知道这是一个子程序,而不是一个变量。另一个没有参数的子程序的例子是System.currentTimeMillis(),来自于System类。执行时,该函数检索当前时间,表示为毫秒,以一个标准化基准时间开始计算(如果你在意的话,是从1970年开始)。一毫秒是1/1000秒。System.currentTimeMillis()的返回值是长整型(一个64位的整数)。此函数可以用来测量计算机执行任务所用的时间。仅仅记录任务开始时间和结束时间,并计算时间差。

这里有一个执行数学任务的示例程序,计算完成会得出执行任务的耗时。有些电脑上的耗时可能是零,因为它用毫秒来测量太小了。即使不是零,可以肯定的是计算机报告的大部分时间是花费在输出或者工作任务上而不是程序上,因为程序中执行的计算只占用计算机时间1毫秒中的很小一部分。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
/* 该程序执行一些数学计算,并显示结果。还显示常量Math.PI的值。
 * 并得出计算机在执行这些任务的耗时。
 */
public class TimedComputation {
   public static void main(String[] args) {
      long startTime; // 程序的启动时间,以毫秒为单位。
      long endTime;   // 计算完成的时间,以毫秒为单位。
      double time;    // 启动时间和完成时间的差,以秒为单位。
      startTime = System.currentTimeMillis();
      double width, height, hypotenuse;  // 三角形的边
      width = 42.0;
      height = 17.0;
      hypotenuse = Math.sqrt( width*width + height*height );
      System.out.print('A triangle with sides 42 and 17 has hypotenuse ');
      System.out.println(hypotenuse);
      System.out.println('nMathematically, sin(x)*sin(x) + '
                                       + 'cos(x)*cos(x) - 1 should be 0.');
      System.out.println('Let's check this for x = 1:');
      System.out.print('      sin(1)*sin(1) + cos(1)*cos(1) - 1 is ');
      System.out.println( Math.sin(1)*Math.sin(1)
                                        + Math.cos(1)*Math.cos(1) - 1 );
      System.out.println('(There can be round-off errors when'
                                      + ' computing with real numbers!)');
      System.out.print('nHere is a random number:  ');
      System.out.println( Math.random() );
      System.out.print('The value of Math.PI is ');
      System.out.println( Math.PI );
      endTime = System.currentTimeMillis();
      time = (endTime - startTime) / 1000.0;
      System.out.print('nRun time in seconds was:  ');
      System.out.println(time);
   } // end main()
} // end class TimedComputation

2.3.2 类和对象

类可以作为静态变量和子程序的容器。然而类还有另外一个作用,即用来描述对象。在这个角色中,类是一种类型,int和double都是类型。也就是类名可以用于声明变量,这样的变量只能有一种类型的值。在这种情况下值是对象。对象是变量和子程序的集合。每个对象都有一个关联的类,表示对象的“类型”。对象的类指定了对象包含的子程序和变量。使用相同的类定义的所有对象包含相同的变量和子程序集合。例如,一个对象可能表示平面上的一个点,它可能包含变量名为x和y来表示这个点的坐标。每一个点对象都会有x和y,但不同的点中,这些变量的值也不同。比如,可能存在一个名为Point的类,定义所有点对象的共同结构,并且所有这些对象都是Point类型的值。

另外一个例子,让我们重新审视System.out.println。System是一个类,out是一个类中的静态变量。然而,System.out是一个对象,并且System.out.println实际上是一个子程序的全称,子程序是包含着对象System.out中的。现在还不需要理解,但是这个被System.out引用的对象是PrintStream类的对象。PrintStream是另一个Java标准库中的类。任何PrintStream类型的对象都是可用于打印信息的目的地;每一个PrintStream类型的对象都有一个println的子程序,用来发送信息到目的地。System.out对象只有一个可能的目的地,而其它PrintStream类型对象可能将发送信息到其它目的地,如其它文件或通过网络发送到其它电脑。这就是面向对象编程:不同的东西中都可以找到共同点,例如这里PrintStream的对象都可用来作为信息的目的地,因此可以用同样的方式使用,如通过一个println子程序。PrintStream类表示这些对象之间的共性。

类的双重角色令人困惑,在实践中大多数类主要或者仅仅为了执行其中的一个角色。幸运的是,你不需要过于担心,在第五章中会对使用对象进行更严谨地讨论。

顺便说一下,因为类名和变量名都使用类似的方法,这可能很难分辨出哪个是哪个。记住Java中所有内置、预定义的名称都是遵循这个规则,即类名以大写字母开头,变量名以小写字母开头。虽然这不是一个正式的语法规则,但是强烈建议你在自己编程时遵循这个规则。子程序的名称也应以小写字母开头。不存在混淆变量和子程序的可能性,因为程序中子程序的名称后面总是跟随一个左括号。

最后还要提醒一下,你应该意识到Java中的子程序通常被称为方法。一般来说,术语“方法(method)”是指包含在类或对象中的子程序。对于Java中每个子程序来说这是正确的,Java中每个子程序都是一个方法。其它编程语言并非如此。目前,更偏向使用更普遍的术语“子程序”。然而,我也注意到有些人从一开始就喜欢使用“方法”这种称呼。

2.3.3 字符串操作

String是类,而String类型的值是对象。对象包含数据,即组成的字符串的字符序列;它还包含子程序。所有的这些子程序实际上都是函数。例如,每个字符串对象包含一个名为length的函数,计算字符串中的字符数。假设advice指的是一个字符串变量,advice可能像下面例子中进行声明和赋值:

1
2
String advice;
advice = 'Seize the day!';

advice.length()是一个函数调用,返回字符串”Seize the day!”中的字符数。在这个例子中,返回值是14。一般来说,对于任何一个字符串类型的变量str,str.length()的返回值是int,即这个字符串中的字符数。注意这个函数没有参数;计算指定字符串长度的是str的值。length子程序在String类中定义,可用于任何字符串类型的值。甚至可用于字符串常量,也就是于字符串类型的常量值。例如,有一个计算字符串”Hello World”的程序如下:

1
2
3
System.out.print('The number of characters in ');
System.out.print('the string 'Hello World' is ');
System.out.println( 'Hello World'.length() );

String类定义了许多函数。这里是一些可能对你有用的函数,假设s1和s2是String类型的变量:

  • s1.equals(s2)是一个返回布尔值的函数。如果s1与s2有着完全相同的字符序列则返回true,否则返回false。
  • s1.equalsIgnoreCase(s2)是另一个布尔值的函数,检查s1与s2是否是相同的字符串,但是这个函数不区分大小写。因此,如果s1是”cat”,那么s1.equals(“Cat”)返回false,而s1.equalsIgnoreCase(“Cat”)返回true。
  • s1.length(),正如上面提到的,是一个返回值是整数的函数,给出了s1中的字符数。
  • s1.charAt(N),其中N是一个整数,返回一个字符类型值。它返回字符串中的第n个字符。位置编号从0开始,所以s1.charAt(0)实际上是第一个字符,s1.charAt(1)是第二个字符,以此类推,最后一个字符是s1.length()- 1。例如,”cat”.charAt(1)的值是’a'。如果参数的值小于零或大于等于s1.length()就会产生错误。
  • s1.substring(N,M),其中N和M是整数,返回一个字符串类型的值。返回值包含s1中位置为N, N+1,…, M-1的字符,注意不包括位置在M的字符。返回值被称为s1的子字符串。子程序s1.substring(N)返回s1中从位置N开始直到字符串结束的子字符串。
  • s1.indexOf(s2)返回一个整数。如果s2 以s1的子串出现,那么返回值是字串在字符串的起始位置。否则,返回的值是-1。读者可以使用s1.indexOf(ch)在s1中搜索字符ch出现的位置。也可以使用s1.indexOf(x,N) 在s1中搜索x第一次或者第N次出现的位置。还可以使用s1.lastIndexOf(x)在s1中搜索x最后一次出现的位置。
  • s1.compareTo(s2)是一个比较两个字符串的整数值函数。如果字符串相同,返回值为零。如果s1小于s2,返回值是一个小于零的数;如果s1大于s2,返回的值是某个大于零的数。(如果这两个字符串完全是由小写字母成或者全由大写字母组成,那么“小于”和“大于”指的是按字母顺序排列。否则,排序更加复杂。)
  • s1.toUpperCase()是一个字符串值函数,返回一个新的等于s1的字符串,除了任何s1中的小写字母转换为大写。例如,”Cat”.toUpperCase()是字符串”CAT”。还有一个s1.toLowerCase()函数。
  • s1.trim()是一个字符串值函数,返回一个新的等于s1的字符串,字符串前面和后面的非打印字符如空格和制表符会被移除。因此,如果s1的值是”fred “,那么s1.trim()是字符串”fred”,删除了原字符串后面的空格。

注意,对于s1.toUpperCase()、s1.toLowerCase()、s1.trim(),s1的值没有改变。而是创建一个新的字符串来代替,并作为函数的值返回。例如,返回值可以用于一个赋值语句中,如”smallLetters = s1.toLowerCase();”。可通过赋值语句”s1 = s1.toLowerCase();”来改变s1的值。

这是另外一个关于字符串的非常有用的示例:使用+运算符连接两个字符串。两个字符串的连接组成一个新的字符串,包括第一个字符串的字符和第二个字符串的所有字符。例如,”Hello” + “World”等于”HelloWorld”。(当然,要小心这些空格如果想要一个连接字符串的空格,它必须存在输入数据中,如”Hello ” + “World”。)

假设name是一个String类型的变量,并且已经与一个人的名字相关联。那么,程序可以通过执行下面这条语句来问候使用者:

1
System.out.println('Hello, '  +  name  +  '.  Pleased to meet you!');

甚至更令人吃惊的是,实际上可以使用+操作符将任何类型的值连接到一个字符串。值被转换成一个字符串,因为如果将它进行标准输出,那么字符串与其它字符串相连接。例如,表达式”Number” + 42等于字符串”Number42″。下面这些语句:

1
2
3
4
System.out.print('After ');
System.out.print(years);
System.out.print(' years, the value is ');
System.out.print(principal);

以用一条语句替换:

1
2
System.out.print('After ' + years +
                    ' years, the value is ' + principal);

很明显,这种方式非常方便,能缩短本章前面介绍的一些例子。

2.3.4 枚举类型介绍

Java有8个内置的基本类型和很多定义为class的类型,如String。但即使这大量的类型并不足以涵盖一个程序员可能需要处理的所有可能的情况。因此,就像任何其它编程语言一样,Java的一个重要组成部分是能够创建新的类型。在大多数情况下,这是通过定义新类实现的,读者将在第5章学习怎么定义新类。但是这里先看一个具体的实例:定义枚举的能力(简称枚举类型)。

从技术上讲,枚举被认为是一种特殊的类,但这并不重要。本节将着眼于枚举的简化形式。在实践中,大多数枚举的使用者只需要这里给出的简化形式。

枚举是一种类型,含有可能值的固定列表,固定列表是在创建枚举时指定的。在某些方面,枚举类似于布尔数据类型,其唯一可能的值是true或false。然而,布尔是基本类型,而枚举不是。

枚举类型的简化定义:

1
enum enum-type-name { list-of-enum-values }

这个定义不能在子程序中,可以将其放在程序的main()外面。enum-type-name可以是任何简单的标识符。这个标识符称为枚举类型的名称,以同样的方式,”boolean”是布尔类型的名称,”String”是字符串类型的名称。在list-of-enum-values中的每个值必须是一个简单的标识符,并且列表中的标识符之间用逗号分隔。例如,下面是一个名为Season的枚举类型的定义,其值是一年中四个季节的名称:

1
enum Season { SPRING, SUMMER, FALL, WINTER }

按照惯例,枚举值给出的名字由大写字母组成,但这是一种风格指南而不是语法规则。枚举值是常量;也就是说,它代表一个无法改变的固定值。枚举类型的可能值通常被称为枚举常量。

注意Season类型的枚举常量被认为是包含在Season中,为了遵循这种规范需要使用符合标识符的约定,这意味着在程序中是这样引用这些名称的:Season.SPRING、Season.SUMMER、Season.FALL和Season.WINTE。

一旦创建了枚举类型,就可以与使用其它类型完全相同的方式来声明变量。例如,用下列语句声明一个名为vacation的Season类型的变量:

1
Season vacation;

声明变量之后,就可以采用赋值语句给变量赋值。赋值语句右边的值是一个Season类型的枚举常量。记得使用常量的全称,包括”Season”!例如:

1
vacation = Season.SUMMER;

使用输出语句可以打印一个枚举值,如System.out.print(vacation)。输出值是枚举常量的名称(没有”Season.”)。在这种情况下,输出的是”SUMMER”。

因为枚举在理论上是一个类,因此枚举值是理论上的对象。作为对象它们可以包含子程序。名为ordinal()是枚举值中的子程序之一。在枚举值的列表中使用一个枚举值时,返回这个值的序数。序数只是告诉列表中值的位置。这就是说,Season.SPRING.ordinal()返回一个整型值0,Season.SUMMER.ordinal()返回1,Season.FALL.ordinal()返回2Season.WINTER.ordinal()返回 3。(到处都能看到,计算机科学家喜欢以零开始计数!)当然,可以对Season类型的变量使用ordinal()方法与,如vacation.ordinal()。

使用枚举使程序更具可读性,因为可以为这些值赋予有意义的名称,同时可以预防某些类型的错误。因为编译器可以检查分配给一个枚举变量的值实际上是否合法。然而,事实上这本书中只是偶尔会用到枚举。目前,你可以将这些当做创建新类型的第一个例子。下面是一个小示例,展示在一个完整的程序中枚举的使用方法:

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
public class EnumDemo {
       // 定义两个枚举类型,注意这个定义位于main()函数之外!
    enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }
    enum Month { JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }
    public static void main(String[] args) {
         Day tgif;     // 定义类型为Day的变量
         Month libra;  // 定义类型为Month的变量
         tgif = Day.FRIDAY;    // 将一个类型为Day的变量赋值给tgif
         libra = Month.OCT;    // 将一个类型为Month的变量赋值给libra
         System.out.print('My sign is libra, since I was born in ');
         System.out.println(libra);   // 输出的值为:OCT
         System.out.print('That's the ');
         System.out.print( libra.ordinal() );
         System.out.println('-th month of the year.');
         System.out.println('   (Counting from 0, of course!)');
         System.out.print('Isn't it nice to get to ');
         System.out.println(tgif);   // 输出的值为:FRIDAY
         System.out.println( tgif + ' is the ' + tgif.ordinal()
                                            + '-th day of the week.');
    }
}

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多