Java学习笔记(2):对象与类

在类之间,最常见的关系有:

  • 依赖(“uses-a”):一个类的方法操纵另一个类的对象
  • 聚合(“has-a”):一个类的对象包含另一个类的对象
  • 继承(“is-a”):特殊对象与一般对象

构造器:一种特殊的方法,用来构造并初始化对象。(类似C++的构造函数?)

构造器的名字应与类名相同。

使用new构造一个对象:

new Date();


可以将这个对象传递给一个方法:

System.out.println(new Date());


可以将一个方法应用到刚刚创建的对象上:

String s = new Date().toString();


当希望多次使用构造出的对象时,可将其保存在一个变量中:

Date birthday = new Date();


 

对象和对象变量

Date deadline; // deadline doesn't refer to any object


该语句定义了一个对象变量deadline,它可以引用Date类型对象。但它不是一个对象,因此不能将Date方法应用于这个变量:

s = deadline.toString(); // 错误!


一个对象变量没有实际包含一个对象,仅仅引用了一个对象:

deadline = birthday; // 两个变量引用同一个对象


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

可显式将对象变量设置为null,表示目前没有引用任何对象:

deadline = null;
...
if (deadline != null)
	System.out.println(deadline);


如果将一个方法应用于一个值为null的对象上,将返回一个运行时错误:

birthday = null;
String s = birthday.toString(); // runtime error!


GregorianCalendar类

Date类的实例有一个状态,即特定的时间点。

GregorianCalendar类用来表示大家熟悉的日历表示法。

GregorianCalendar类扩展了一个更加通用的Calendar类,它描述了日历的一般属性。

Date类只提供了少量的方法用来比较两个时间点,如before和after:

if (today.before(birthday))
	System.out.println("Still time to shop for a gift.");

GregorianCalendar类包含的方法比Date类要多得多,特别是有几个很有用的构造器。
new GreogrianCalendar()

构造一个新对象,用于表示对象构造时的日期和时间。

还可以通过提供年、月、日构造一个表示某个特定日期午夜的日历对象:

new GregorianClendar(1999, 11, 31) // 月份从0计数,11表示十二月
new GregorianCalendar(1999, Calendar.DECEMBER, 31)

还可以设置时间:

new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)


查询信息通常使用get方法。

查询GregorianCalendar类的信息时通常需要使用Calendar类中定义的一些常量:

GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);

调用set方法,可以改变对象的状态:
deadline.set(Calendar.YEAR, 2001);
deadline.set(Calendar.MONTH, Calendar.APRIL);
deadline.set(Calendar.DAY_OF_MONTH, 15);

可同时设置年、月、日:
deadline.set(2001, Calendar.APRIL, 5);

可以为给定的日期对象增加天数、星期数、月份等:
deadline.add(Calendar.MONTH, 3); // move deadline by 3 months


GregorianCalendar类有getTime方法和setTime方法,用来获得和设置日历的时间点:
Date time = calendar.getTime();
calendar.setTime(time);

这些方法使用在GregorianCalendar类和Date类之间的转换:

GregorianCalendar calendar = new GreogrianCalendar(year, month, day);
Date hireDay = calendar.getTime();

GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);

示例:打印当前月的日历
import java.text.DateFormatSymbols;
import java.util.*;

public class CalendarTest
{
	public static void main(String[] args)
	{
		// 设置地区(不同地区有着不同的日期表示习惯)
		Locale.setDefault(Locale.US);
		
		GregorianCalendar d = new GregorianCalendar();
		int today = d.get(Calendar.DAY_OF_MONTH);
		int month = d.get(Calendar.MONTH);
		d.set(Calendar.DAY_OF_MONTH, 1);	// 设置d为当月第一天
		int weekday = d.get(Calendar.DAY_OF_WEEK);
		
		int firstDayOfWeek = d.getFirstDayOfWeek(); // 当前地区星期的起始日
		int indent = 0;
		while (weekday != firstDayOfWeek)
		{
			indent++;
			d.add(Calendar.DAY_OF_MONTH, -1);
			weekday = d.get(Calendar.DAY_OF_WEEK);
		}
		
		// 获取表示星期几个英文缩写
		String[] weekdayNames = new DateFormatSymbols().getShortWeekdays();
		do
		{
			System.out.printf("%4s", weekdayNames[weekday]);
			d.add(Calendar.DAY_OF_MONTH, 1);
			weekday = d.get(Calendar.DAY_OF_WEEK);
		} while (weekday != firstDayOfWeek);
		System.out.println();
		
		for (int i = 1; i <= indent; i++)
			System.out.print("    ");
		d.set(Calendar.DAY_OF_MONTH, 1);
		do
		{
			int day = d.get(Calendar.DAY_OF_MONTH);
			System.out.printf("%3d", day);
			
			if (day == today) System.out.print("*");
			else System.out.print(" ");
			
			d.add(Calendar.DAY_OF_MONTH, 1);
			weekday = d.get(Calendar.DAY_OF_WEEK);
			
			if (weekday == firstDayOfWeek) System.out.println();			
		} while (d.get(Calendar.MONTH) == month);
		
		if (weekday != firstDayOfWeek) System.out.println();
	}
}


用户自定义类

最简单的类定义形式:

class ClassName
{
	constructor1
	constructor2
	...
	method1
	method2
	...
	field1
	field2
	...
}
下面是一个非常简单的Employee类:
class Employee
{
	// constructor
	public Employee(String n, double s, int year, int month, int day)
	{
		name = n;
		salary = s;
		GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
		hireDay = calendar.getTime();
	}
	
	// a method
	public String getName()
	{
		return name;
	}
	
	// more methods
	...
	
	// instance fields
	
	private String name;
	private double salary;
	private Date hireDay;
}

 多个源文件的使用

一个源文件可包含多个类,也可将每个类存放在单独的原文件中,如Employee类存放在Employee.java,EmployeeTest类存放在EmployeeTest.java。

这样组织文件,有两种编译方法:

1. 使用通配符:

javac Employee*.java

 2. 只编译主源文件:

javac EmployeeTest.java
当编译器发现使用了Employee类时会查找Employee.class文件,若未找到,就会查找Employee.java,并对其进行编译。

与C++不同,Java中所有的方法都必须的类内定义。


Final实例域

可以将实例域定义为final。构建对象时必须初始化这样的域,并且在后面的操作中,不能够再对它进行修改。如:

class Employee
{
	...
	private final String name;
}
final修饰符大都应用于基本数据类型域,或不可变类的域。

对于可变类,使用final修饰符可能会对读者造成混乱,如:

private final Date hiredate;

仅仅意味着存储在hiredate变量中的对象引用在对象构造之后不能被改变,而并不意味着hiredate对象是一个常量。任何方法都可以对hiredate引用的对象调用setTime更改器。


静态域和静态方法

如果将域定义为static,每个类中只有一个这样的域。而每个对象对于所有的实例域却都有自己的一份拷贝。

假定需要给每一个雇员赋予一个惟一的标识码,添加一个实例域id和一个静态域nextId:

class Employee
{
	...
	private int id;
	private static int nextId = 1; // 属于类,不属于任何对象
}
现在,每一个对象都有一个自己的id域,但这个类的所有实例将共享一个nextId域。

静态常量

public class Math
{
	...
	public static final double PI = 3.14159265358979323846;
	...
}
在程序中,可以使用math.PI获得这个常量。

如果关键字static被省略,PI就变成了Math类的一个实例域。需要通过Math类的对象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。

静态方法

静态方法是一种不能向对象实施操作的方法。如Math类的pow方法:

Math.pow(x, a)
计算x的a次方。运算时,不使用任何Math对象。

在下面两种情况下使用静态方法:

  • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供。
  • 一个方法只需要访问类的静态域。

Factory方法
静态方法还有一种常见的用途。NumberFormat类使用factory方法产生不同风格的格式对象。
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%
NumberFormat类不利用构造器完成这些操作的原因:
  • 无法命名构造器。
  • 当使用构造器时,无法改变所构造的对象类型。

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

方法参数
方法参数共有两种类型:
  • 基本数据类型(数字型和布尔型)
  • 对象引用
Java总是采用“值调用”,特别的是,方法不能修改传递给它的任何参数变量的内容(修改的只是一个拷贝):
public static void tripleValue(double x) // doesn't work
{
	x = 3 * x;
}

然而,方法可以修改一个对象引用的参数:
public static void tripleSalary(Employee x) // works
{
	x.raiseSalary(200);
}
总结:
  • 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能实现让对象参数引用一个新的对象。

对象构造

重载:多个方法可以有相同的名字,不同的参数(返回类型不起作用)。

默认构造器:没有参数的构造器

public Employee()
{
	name = "";
	salary = 0;
	hireDay = new Date();
}
一个类没有任何构造器,则系统会提供一个默认构造器,将所有实例域设置为默认值。

一个类若有至少一个构造器,但没有默认构造器,则系统不提供默认构造器,若构造对象时不提供构造参数则被视为不合法。

可以在类定义域中,直接将一个值赋给任何域。在执行构造器之前,先执行赋值操作。初始值不一定是常量,也可以调用方法:

class Employee
{
	...
	static int assignId()
	{
		int r = nextId;
		nextId++;
		return r;
	}
	...
	private int id = assignId();
}
C++中,不能直接初始化实例域。

Java中,木有初始化列表。


参数名

三种风格:

// 参数用单个字母,编写小构造器时常用
public Employee(String n, double s)
{
	name = n;
	salary = s;
}

// 参数前加前缀“a”,这样更易阅读
public Employee(String aName, double aSalary)
{
	name = aName;
	salary = aSalary;
}

// 参数屏蔽实例域,使用this访问
public Employee(String name, double aSalary)
{
	 this.name = name;
	 this.salary = salary;
}


调用另一个构造器

public Employee(double s)
{
	// calls Employee(String, double)
	this("Employee #" + nextId, s);
	nextId++;
}
当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String, double)构造器。

采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。


初始化块

在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行:

class Employee
{
	public Employee(String n, double s)
	{
		name = n;
		salary = s;
	}
	public Employee()
	{
		name = "";
		salary = 0;
	}
	...
	private static int nextId;
	
	private int id;
	private String name;
	private double salary;
	...
	
	// object initialization block
	{
		id = nextId;
		nextId++;
	}
}
该示例中,无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。(这种机制不常用)


调用构造器的具体处理步骤:

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

静态的初始化块

如果对类的静态域进行初始化的代码比较复杂,那么可使用静态的初始化块:

// static initialization block
static
{
	Random generator = new Random();
	nextId = generator.nextInt(10000);
}
在类第一次加载时,会进行静态域的初始化。所有静态初始化语句以及静态初始化块都将依照类定义的顺序执行。


对象析构与finalize方法

Java有自动的垃圾回收器,不需要人工回收内存。

某些对象使用了内存之外的其他资源,当资源不再需要时,需要则需将其回收。

可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。


包(package)

Java允许使用包将类组织起来。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

使用包的主要原因是确保类名的惟一性。


类的导入

一个类可以使用所属包中的所有类,以及其他包中的公有类。

可以采用两种方式访问另一个包中的公有类。

1. 每个类名前添加完整的包名:

java.util.Date today = new java.util.Date();
2. 使用import语句导入一个特定的类或者整个包:
import java.util.*;
然后就可使用:
Date today = new Date();
C++中,与包机制类似的是命名空间(namespace),不是头文件(#include)。


静态导入

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

import static java.lang.System.*;
import static java.lang.System.out;

静态导入不利于代码的清晰度。但是有两个实际的应用:

  • 算术函数。
  • 笨重的常量。

将类放入包中
要想将一个类放入包中,就必须将包的名字放在源文件开头,包中定义类的代码之前:
package com.horstmann.corejava;

public class Employee
{
	...
}
若不加入package语句,这个源文件中的类就放在一个默认包(deafult package)中。
将包中的文件放到与完整的包名匹配的子目录中。如com.horstmann.corejava包中的所有源文件应该被放置在子目录com\horstmann\corejava中。

包作用域
标记为public的部分可以被任意的类使用,标记为private的部分只能被定义它们的类使用,没有指定public或private,这个部分可以被同一个包中的所有方法访问。(和C++不同)

类路径
为了使类能够被多个程序共享,需要做到下面几点:
  1. 把类放到一个目录中,如/home/user/classdir。
  2. 将JAR文件放在一个目录中,如/home/user/archives。
  3. 设置类路径(class path)。类路径是所有包含类文件的路径的集合。
类路径包括:
基目录/home/user/classdir或c:\classes。
当前目录(.)。
JAR文件/home/user/archives/archive.jar或c:\archives\archive.jar。
最好采用-calsspath(或-cp)选项指定类路径:
java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg.java
或者
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg.java

文档注释
JDK包含一个叫javadoc的工具,它可以由源文件生成一个HTML文档。
javadoc实用程序(utility)从下面几个特性中抽取信息:
  • 公有类与接口
  • 公有的和受保护的方法
  • 公有的和受保护的域
应该为上面几部分编写注释。注释应放在所描述特性的前面,注释以/**开始,并以*/结束。
每个/**...*/文档注释在标记之后紧跟着自由格式文本(free-form text)。标记由@开始,如@author或@param。
自由格式文本的第一句应是一个概要性句子。javadoc实用程序自动地将这些句子抽取出来形成概要页。
自由格式文本中,可以使用HTML修饰符。

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

方法注释必须放在所描述的方法之前。除了通用标记外,还可使用:
@param variable description
@return description
@throws class descrption

例:

/**
 * Raises the salary of an employee.
 *  @param byPercent the percentage by which to raise the salary(e.g. 10 = 10%)
 *  @return the amount of the raise
 */
 public double raiseSalary(double byPercent)
 {
	double raise = salary * byPercent / 100;
	salary += raise;
	return raise;
 }

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


通用注释

  • @author name
  • @version text
  • @since text(始于条目)
  • @deprecated text(不再使用注释)
  • @see reference(在“see also”部分增加一个超级链接),reference可以选择下列情形之一

package.class#feature label

<a href="...">label<a>

"text"

第一种最常见,如:

@see com.horstmann.corejava.Employee#raiseSalary(double)
建立一个链接到com.horstmann,corejava,Employee类的raiseSalary(double)方法的超链接。

  • 如果愿意的话,可以在注释中的任何位置放置指向其他类或方法的超链接,以及插入一个专用标记,如:

{@link package.class#feature label}


包与概述注释

要想产生包注释,就需要在每一个包目录中添加一个单独的文件,可以有如下两种选择:

  • 提供一个以package.html命名的HTML文件。在标记<BODY>...</BODY>之间的所有文本都会被抽取出来。
  • 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/**和*/界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为overview.html的文件中。这个文件位于包含所有源文件的父目录中。标记<BODY>...</BODY>之间的所有文本将被抽取出来。

注释的抽取
假设HTML文件将被存放在目录docDirectory下,执行以下步骤:
1. 切换到包含想要生成文档的源文件目录。
2. 如果是一个包,运行命令:
javadoc -d docDirectory nameOfPackage

或对于多个包生成文档,运行:
javadoc -d docDirectory nameOfPackage1 nameOfPackage2...
如果文件在默认包中,应运行:
javadoc -d docDirectory *.java

类设计技巧
  1. 一定要将数据设计为私有
  2. 一定要对数据初始化
  3. 不要在类中使用过多的基本数据类型
  4. 不是所有的域都需要独立的域访问器或域更改器
  5. 使用标准格式进行类的定义
  6. 将职责过多的类进行分解
  7. 类名和方法名要能够体现它们的职责

一般采用如下顺序书写类的内容:
  • 公有访问特性部分
  • 包作用域访问特性部分
  • 私有访问特性部分
每一部分中,应按照下列顺序列出:
  • 实例方法
  • 静态方法
  • 实例域
  • 静态域

猜你喜欢

转载自blog.csdn.net/angel_lys/article/details/8497455