类和对象的UML表示
对象(object)是现实世界中可识别的实体,具有状态和行为。状态是其属性的当前值,行为是一系列方法,这些方法可改变对象的状态。
类(class)定义或封装同类对象共有的属性和方法,即将同类型对象共有的属性和行为抽象出来形成类的定义。
例如要开发学生管理系统,根据应用需求,我们发现所有学生的以下共有属性和行为需要管理
- 属性:学号、姓名、性别、所在学院、年级、班级
- 行为:考试、上课、完成作业
由此形成类的定义:Class Student{ … },属性作为数据成员,行为作为方法成员
同一类型的对象有相同的属性和方法,但每个对象的属性值不同。
类是对象的模板,而对象是类的实例。
当定义好类Student,可以用类型Student去实例化不同对象代表不同学生
Student s = new Student(…)
UML是面向对象设计的建模工具,独立于任何具体程序设计语言。
UML有严格的语法和语义规范。对于复杂系统,先用UML建模,再编写代码。UML工具会自动把模型编译成Java(C++)源码(方法体是空的)
UML采用一组图形符号来描述软件模型,这些图形符号简单、直观和规范。所描述的软件模型,可以直观地理解和阅读,由于具有规范性,能保证模型的准确、一致。
成员访问权限:公有public用 + 表示,保护protected用 # 表示,私有private用 - 表示,包级用 ~ 表示或默认无表示(包级即可以被同一个package的代码访问的成员。Java无friend,无析构函数,垃圾自动回收)
定义类与创建对象
Java无struct 和 union
class Circle{double radius = 1.0;//数据成员Circle(){radius = 1.0;}Circle(double r){radius = r;}//构造函数double findArea(){return radius * radius * 3.14159;}//方法
}
Circle c1=new Circle(), c2=new Circle(10.0), c3=new Circle(15.0);
与基本数据类型一样,可声明并用new创建对象数组。
int[]a = new int[10]; //所有元素缺省初值=0
创建对象数组时,数组元素的缺省初值为null。
Circle[] circleArray = new Circle[10]; //这时没有构造Circle对象,只是构造数组
for(int i = 0; i < circleArray.length; i++) {circleArray[i] = new Circle(); //这时才构造Circle对象,可使用有参构造函数
}
构造函数
无返回类型,名字同类名,用于初始化对象。
如果定义void className(…),被认为是普通方法,只在new时被自动执行。
必须是实例方法(无static),可为公有、保护、私有和包级权限。
类的变量为引用(相当于C指针),指向实例化好的对象。
Circle c2=new Circle(5.0);//调用时必须有括弧,可带参初始化
缺省构造函数(同C++)
- 如果类未定义任何构造函数,编译器会自动提供一个不带参数的默认构造函数。
- 如果已自定义构造函数,则不会提供默认构造函数。
- Java没有析构函数,但垃圾自动回收之前会自动调用finalize()。可以覆盖定义该函数(但是finalize调用时机程序员无法控制)。
public class ConstructorTest {//构造函数前面不能有void public ConstructorTest() {System.out.println("constructor");}//如果和类名同名函数前面加了void(可返回任何类型), 编译器看成是普通函数,这和C++不一样 public void ConstructorTest() {System.out.println("normal instance method return void");}public double ConstructorTest(double d) {System.out.println("normal method return double");return d;}public static void main(String ... args){//先调用构造,再调用void ConstructorTest() new ConstructorTest().ConstructorTest();}
}
对象访问
访问对象:通过对象引用访问。JVM维护每个对象的引用计数器,只要引用计数器为0,该对象会由JVM自动回收。
通过对象引用,可以
访问对象的实例变量(非静态数据字段):c2.radius。
调用对象的实例方法:c2.findArea()。通过c2调用实例方法时,c2引用会传给实例方法里的this引用。
访问静态成员和静态方法(不推荐,推荐用类名)
在实例方法中有个this引用,代表当前对象(引用当前对象:相当于指针),因此在实例方法里,可以用this引用访问当前对象成员
this.radius
this.findArea();
- 在构造函数中调用构造函数,须防止递归调用
- 不能对this进行赋值
匿名对象也可访问实例(或静态)成员:new Circle().radius=2;
public class Circle {double radius = 1.0;Circle() {radius = 1.0;}Circle(double r) {this.radius = r;}double findArea() {return radius * radius * Math.PI;}public void setRadius(double newRadius){this.radius = newRadius;}
}
public class TestSimpleCircle {public static void main(String[] args){Circle c1 = new Circle();System.out.println("Area = " + c1.findArea() + ", radius = " + c1.radius);Circle c2 = new Circle(10.0);System.out.println("Area = " + c2.findArea() + ", radius = " + c2.radius);//modify radiusc2.setRadius(20.0);System.out.println("Area = " + c2.findArea() + ", radius = " + c2.radius);}
}
与基本数据类型变量不同,引用变量表示数据的内存单元地址或存储位置。引用类型变量存储的是对象的引用。当变量未引用任何对象或未实例化时,它是值为null。
数组和类是引用类型变量,引用了内存里的数组或对象,每个对象(数组)有引用计数,一个对象的引用计数=0时被自动回收。
对象作为方法参数时与传递数组一样,传递对象实际是传递对象的引用。
基本数据类型传递的是实际值的拷贝,传值后形参和实参不再相关:修改形参的值,不影响实参。引用类型变量传递的是对象的引用,通过形参修改对象object,将改变实参引用的对象object。
Java无类似C++的&来修饰方法参数,只能靠形参的声明类型来区分是传值还是传引用。
包(package)
包是一组相关的类和接口的集合。将类和接口分装在不同的包中,可以避免重名类的冲突,更有效地管理众多的类和接口。
package就是C++里的namespace
包的定义通过关键字package来实现:package 包名;
package语句必须出现在.java文件第一行,前面不能有注释行也不能有空白行,该.java文件里定义的所有内容(类、接口、枚举)都属于package所定义的包里。如果.java文件第一行没有package语句,则该文件定义的所有内容位于default包(缺省名字空间),但不推荐。
不同.java文件里的内容都可以属于同一个包,只要它们第一条package语句的包名相同,包是逻辑上的结构,可以跨越多个物理的.java文件。
package本质上就是C++里的namespace,因此
在同一个package里不能定义同名的标识符(类名,接口名,枚举名)。例如一个类名和一个接口名不能相同
如果要使用其它包里标识符,有二个办法:
用完全限定名,例如要调用java.util包里的Arrays类的sort方法: java.util.Arrays.sort(list);
在package语句后面,先引入要使用其它包里的标识符,再使用:
import java.util.Arrays; //或者: import java.util.*; Arrays.sort(list);
import语句可以有多条,分别引入多个包里的名字。
使用二种import的区别:
单类型导入(single type import):导入包里一个具体的标识符,如
import java.util.Arrays;
按需类型导入(type import on demand):并非导入一个包里的所有类,只是按需导入
import java.util.*;
二种导入的区别类似C++里二种使用名字空间方式的区别:
单类型导入:把导入的标识符引入到当前.java文件,因此当前文件里不能定义同名的标识符,类似C++里 using nm::id; 把名字空间nm的名字id引入到当前代码处
按需导入:不是把包里的标识符都引入到当前.java文件,只是使包里名字都可见,使得我们要使用引入包里的名字时可以不用使用完全限定名,因此在当前.java文件里可以定义与引入包里同名的标识符。但二义性只有当名字被使用时才被检测到。类似于C++里的using nm;
比如有:
package p1;public class A {}
导入方法1:
package p2;//单类型导入,把p1.A引入到当前域 import p1.A;//这个时候当前文件里不能定义A, //下面语句编译报错 public class A {}
导入方法2:
package p2;import p1.*; //按需导入,没有马上把p1.A引入到当前域//因此当前文件里可以定义A public class A {public static void main(String[] args){A a1 = new A(); //这时A是p2.ASystem.out.println(a1 instanceof p2.A); //true//当前域已经定义了A,因此要想使用package p1里的A,//只能用完全限定名p1.A a2 = new p1.A();} }
又比如有:
package p1;public class A {}
package p2;public class A {}
这时有:
package p3; //可以按需导入,没有马上把p1.A,p2.A引入到当前域 //因此下面二个import不会保错 import p1.*; import p2.*;public class B { //当名字被使用时二义性才被检测A a; //报错,Reference to A is a ambiguous, p1.A and p2.A match; p1.A a1; //这时只能用完全限定名p2.A a2; }
package p3;import p1.A; import p2.A; //报错,p1.A is already defined in a single type importpublic class B {}
包还有个很重要的作用:提供了package一级的访问权限控制(在Java里,成员访问控制权限除了公有、保护、私有,还多了包一级的访问控制;类的访问控制除了public外,也多了包一级的访问控制)
数据成员的封装
面向对象的封装性要求最好把实例成员变量设为私有的或保护的
同时为私有、保护的实例成员变量提供公有的get和set方法。
设成员为DateType propertyName。
get用于获取成员值:public DateType getPropertyName( );
set用于设置成员值:public void setPropertyName(DateType value)
class Circle{private double radius=1.0; //数据成员设为私有public Circle( ){ radius=1.0; }public double getRadius( ){ return radius; }public void setRadius(double r){ radius=r; }
}
class Circle {private double radius;/** 私有静态变量,记录当前内存里被实例化的Circle对象个数*/private static int numberOfObjects = 0; public Circle() { radius = 1.0; numberOfObjects++; }public Circle(double newRadius) { radius = newRadius; numberOfObjects++; }public double getRadius() {return radius;}public void setRadius(double newRadius) { radius = newRadius;}/** 公有静态方法,获取私有静态变量内容*/public static int getNumberOfObjects() {return numberOfObjects;}/** Return the area of this circle */public double findArea() { return radius * radius * Math.PI; }@Overridepublic void finalize() throws Throwable {numberOfObjects--; //对象被析构时,计数器减1super.finalize();}
}
覆盖从Object继承的finalize方法,该方法在对象被回收时调用,方法里对象计数器-1。注意该方法调用时机不可控制。
@Override是注解(annotation)告诉编译器这里是覆盖父类的方法。编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错。例如,你如果没写@Override,而你下面的方法名又写错了,这时你的编译器是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法。
Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner的实现利用了幻象引用(PhantomReference),一种常见的所谓 post-mortem 清理机制。利用幻象引用和引用队列(Java 的各种引用),可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比 finalize 更加轻量、更加可靠。
吸取了 finalize 里的教训,每个 Cleaner 的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。
实例(或静态)的变量、常量和方法
实例变量(instance variable):未用static修饰的成员变量,属于类的具体实例(对象),只能通过对象访问,如“对象名.变量名” 。实例变量是作为对象内存的一部分存在。
静态变量(static variable):用static修饰的变量,被类的所有实例(对象)共享,也称类变量。可以通过对象或类名访问,提倡“类名.变量名”访问。静态变量是单独的内存单元,与对象内存分开。
实例常量是没有用static修饰的final变量。
静态常量是用static修饰的final变量。如Math类中的静态常量PI定义为:
public static final double PI = 3.14159265358979323846;
所有常量可按需指定访问权限,不能用等号赋值修改。由于不能被修改,故通常定义为public。
final也可以修饰方法
final修饰实例方法时,表示该方法不能被子类覆盖(Override) 。非final实例方法可以被子类覆盖。
final修饰静态方法时,表示该方法不能被子类隐藏(Hiding)。非final静态方法可以被子类隐藏。
构造函数不能为final的。
方法重载(Overload)、方法覆盖(Override)、方法隐藏(Hiding)
方法重载:同一个类中、或者父类子类中的多个方法具有相同的名字,但这些方法具有不同的参数列表(不含返回类型,即无法以返回类型作为方法重载的区分标准)
方法覆盖和方法隐藏:发生在父类和子类之间,前提是继承。子类中定义的方法与父类中的方法具有相同的方法名字、相同的参数列表、相同的返回类型(也允许子类中方法的返回类型是父类中方法返回类型的子类)
方法覆盖:实例方法
方法隐藏:静态方法
public class A {public void m(int x, int y) {}public void m(double x, double y) {}//下面语句报错m(int,int)已经定义, 重载函数不能通过返回类型区分 // public int m(int x, int y) { return 0;}; }class B extends A{ //B继承了Apublic void m(float x, float y) { } //重载了父类的m(int,int)和m(double,double)public void m(int x, int y) {} //覆盖了父类的void m(int,int),注意连返回类型都必须一致//注意下面这个语句报错,既不是覆盖(与父类的void m(int,int)返回类型不一样)// 也不是合法的重载(和父类的m(int,int)参数完全一样,只是返回类型不一致 // public int m(int x, int y) {} //错误//子类定义了新的重载函数int m()public int m(){return 0;}; }
class A{public void m1(){ }public final void m2() { }public static void m3() { }public final static void m4() { }
}class B extends A{//覆盖父类A的void m1()public void m1(){ }//下面语句报错,不能覆盖父类的final 方法
// public void m2(){ }public static void m3() { } //隐藏了父类的static void m3()//下面语句报错,父类final 静态方法不能被子类隐藏
// public static void m4() { }
}
静态方法(static method)是用static修饰的方法。构造函数不能用static修饰,静态函数无this引用。
每个程序必须有public static void main(String[])方法。
静态方法可以通过对象或类名调用。
静态方法内部只能访问类的静态成员 (因为实例成员必须有实例才存在,当通过类名调用静态方法时,可能该类还没有一个实例)
静态方法没有多态性。
可见性修饰符
类访问控制符:public和包级(默认);
类的成员访问控制符:private、protected、public和包级(默认)
Java继承时无继承控制(都是公有继承,和C++不同),故父类成员继承到派生类时访问权限保持不变(除了私有)。
成员访问控制符的作用:
- private: 只能被当前类定义的函数访问。
- 包级:无修饰符的成员,只能被同一包中的类访问。
- protected:子类、同一包中的类的函数可以访问。
- public: 所有类的函数都可以访问。
访问控制针对的是类型而不是对象级别
public class Foo{private boolean x;public void m(){Foo foo = new Foo();//因为对象foo在Foo类内使用,所以可以访问私有成员x,并不是只能访问this.xboolean b = foo.x //ok}
}
public class Test{ public static void main(String[] args){Foo foo = new Foo();//因为对象foo在Foo类外使用,所以不可以访问foo的私有成员xboolean b = foo.x //error}
}
子类类体中可以访问从父类继承来的protected成员 。但如果子类和父类不在同一个包里,子类里不能访问另外父类实例(非继承)的protected成员。
package p1;
public class A {protected int i= 0;
}
package p2;
import p1.*;
public class B extends A {protected int j= 0;
}
在B的函数里,可以通过super.i访问到从A继承的i(因为super.i是自己的内存布局一部分)。但是在B的函数里,不能访问另外的对象other的i,因为other对象和this对象是不同内存,除非B和A在一个包里。
大多数情况下,构造函数应该是公有的
有些特殊场合,可能会防止用户创建类的实例,这可以通过将构造函数声明为私有的来实现。
例如,包java.lang中的Math类的构造函数为私有的,所有的数据域和方法都是静态的,可以通过类名直接访问而不能实例化Math对象。
private Math(){}
类的成员变量(实例变量和静态变量)的作用域是整个类,与声明的位置无关。
如果一个成员变量的初始化依赖于另一个变量,则另一个变量必须在前面声明。
public class Foo {int i;//成员变量默认初始化,new后成员默认值为0或null,函数局部变量须初始化int j = i + 1;int f( ){ int i=0; return i+this.i; } //局部变量i会优先访问
} //作用域越小,被访问的优先级越高
如函数的局部变量i与类的成员变量i名称相同,那么优先访问局部变量i,成员变量i被隐藏(可用this.实例变量、this.类变量或类名.类变量发现)。
嵌套作用域不能定义同名的局部变量;但类的成员变量可以和类的方法里的局部变量同名
This引用
this引用指向调用某个方法的当前对象
在实例方法中,实例变量被同名局部变量或方法形参隐藏,可以通过this.instanceVariable访问实例变量。
调用当前类的其它构造函数,需防止递归调用。
this(actualParameterListopt)
必须是构造函数的第1条语句。