05 消息、实例和初始化
1.消息
对象间相互请求或相互协作的途径。
面向对象消息模型
- 对象作为用传递消息的方式互相联系的通信实体 ,既可以接收也可以拒绝外界发来的消息
对象接收它能识别的消息
拒绝它不能识别的消息
对象不可能在不可预知或事先不允许的方式下与其他对象交互
发送消息的流程
考虑对象A向对象B发送消息,也可以看成对象A向对象B请求服务
- 对象A要明确知道对象B提供什么样的服务
- 根据请求服务的不同,对象A可能需要给对象B一些额外的信息,以使对象B明确知道如何处理该服务
- 对象B可将最终的执行结果以报告形式反馈回去
消息传递语法
- 消息→接收器。
- 响应行为随接收器不同而不同。
2.静态与动态语言
动/静态类型语言
- 动态类型语言(Dynamically Typed Language ):变量看作名称标识,类型和数值联系在一起
- 类型的检查是在运行时做的;
- 一般在变量使用之前不需要声明变量类型,而变量的类型通常是由被赋的值的类型决定。
- 优点是方便阅读,不需要写非常多的类型相关的代码
- 缺点是不方便调试,命名不规范时会造成读不懂,不利于理解等
- 如php、Python和Ruby。
- 静态类型语言(Statically Typed Language ):类型和变量联系在一起
- 类型的检查是在编译时做的.
- 在编译时,便需要确定类型的语言。即写程序时需要明确声明变量类型。
- 如C/C++、Java、C#等。
- 优点在于其结构非常规范,便于调试,方便类型安全;
- 缺点是为此需要写更多的类型相关代码,导致不便于阅读、不清晰明了
- 编译时作出内存分配决定。不必运行时刻重新分配。
- 控制类型错误。
面向对象静态
C++,Delphi pascal,Eiffel,Java
面向对象动态
Objective-c,Smalltalk,Dylan,Python
非面向对象静态
Ada,Algol,C,Fortran,Haskell,ML,Modula
非面向对象动态
APL,Forth,Lisp,Prolog,Snobol
动态类型语言与静态类型语言之间的差异在于变量或数值是否具备类型这种特性。
伪变量
- 大多数面向对象语言中,接收器并不出现在方法的参数列表中,而是隐藏于方法的定义之中。只有当必须从方法体内部去存取接收器的数值时,才会使用伪变量(pseudo-variable)。
- 伪变量和通常的变量很相似,只是它不需要声明,也不能被更改(也许用伪常量这一术语更加合适,但是这一术语好像没有出现在任何语言的定义中)
Java, C++: this
Eiffel: Current
Smalltalk, object-c: self
伪变量在使用时就好像是作为类的一个实例。
- 在很多编程语言中,对接收器伪变量的使用都可以忽略。
- 如果在没有引用接收器的条件下,访问一个数据字段或者调用一个方法,那么这意味着接收器伪变量将作为消息的主体。
Java中this的使用
- 使用this关键字引用成员变量
- 使用this关键字在自身构造方法内部引用其它构造方法
- 使用this关键字引用成员方法
- 使用this关键字代表自身类的对象
使用this关键字引用成员变量
在一个类的方法或构造方法内部,可以使用“this.成员变量名”这样的格式来引用成员变量名,有些时候可以省略,有些时候不能省略
public class ReferenceVariable {
private int a;
public ReferenceVariable(int a){
this.a = a;
}
public int getA(){
return a;
}
public void setA(int a){
this.a = a;
}
}
java语言规定当变量作用范围重叠时,作用域小的变量覆盖作用域大的变量。所以在构造方法和setA方法内部,参数a起作用。
这样如果访问成员变量a则必须使用this进行引用。
引用构造方法
public class ThisTest {
private int i=0;
// 第一个构造器:有一个String型形参
ThisTest(String s){
System.out.println("String constructor: "+s);
}
// 第二个构造器:有一个int型形参和一个String型形参
ThisTest(int i,String s){
this(s);//this调用第一个构造器
this.i=i++;//this以引用该类的成员变量
System.out.println("Int constructor: "+i+"/n"+"String constructor: "+s);
}
注意:
- 其他任何方法都不能调用构造器,只有构造方法能调用它。
- 就算是构造方法调用构造器,也必须位于其第一行,构造方法也只能调用一个且仅一次构造器!
使用this关键字代表自身类的对象
public class ThisTest {
private int i=0;
//第一个构造器:有一个int型形参
ThisTest(int i){
this.i=i+1;//此时this表示引用成员变量i,而非函数参数i
System.out.println("Int constructor i——this.i: "+i+"——"+this.i);
System.out.println("i-1:"+(i-1)+"this.i+1:"+(this.i+1));
//从两个输出结果充分证明了i和this.i是不一样的!
}
public ThisTest increment(){
this.i++;
return this;//返回的是当前的对象,该对象属于(ThisTest)
}
public static void main(String[] args){
ThisTest t0=new ThisTest(10);
System.out.println(t0.increment().increment().increment().i);
//t0.increment()返回一个在t0基础上i++的ThisTest对象,
//接着又返回在上面返回的对象基础上i++的ThisTest对象!
}
}
Java:构造函数中,使用this区分参数和数据成员。
3.对象创建
类的实例
- 属于某个类的对象称为该类的一个实例(instance)
- 对象和类间具有instance-of关系
- 一个实例是从一个类创建而来的对象
- 类描述了这个实例的行为(方法)及结构(属性)
类及实例具有下面特征
- 同一个类的不同实例具有相同的数据结构,承受的是同一方法集合所定义的操作,因而具有相同的行为
- 同一个类的不同实例可以持有不同的值,因而可以有不同的状态
- 实例的初始状态(初值)可以在实例化中确定
对象=状态(实例变量)+行为(方法)
- 对象外部看,客户只能看到对象的行为;对象内部看,方法通过修改对象的状态,以及和其他对象的相互作用,提供了适当的行为
内存划分:对象创建之初
public class ClassDemo02 {
public static void main(String args[]){
Person per = new Person() ;
}
}
public class ClassDemo03 {
public static void main(String args[]){
Person per = new Person() ;
per.name = "张三" ; // 为属性赋值
per.age = 30 ;
per.tell() ; // 调用类中的方法
}
}
声明多个变量的情况:
public class ClassDemo04 {
public static void main(String args[]) {
Person per1 = null; // 声明per1对象
Person per2 = null; // 声明per2对象
per1 = new Person(); // 实例化per1对象
per2 = new Person(); // 实例化per2对象
per1.name = "张三"; // 设置per1对象的name属性内容
per1.age = 30; // 设置per1对象的age属性内容
per2.name = "李四"; // 设置per2对象的name属性内容
per2.age = 33; // 设置per2对象的age属性内容
System.out.print("per1对象中的内容 --> ") ;
per1.tell(); // per1调用方法
System.out.print("per2对象中的内容 --> ") ;
per2.tell(); // per2调用方法
}
}
public class ClassDemo05 {
public static void main(String args[]) {
Person per1 = null; // 声明per1对象
Person per2 = null; // 声明per2对象
per1 = new Person(); // 只实例化per1一个对象
per2 = per1 ;// 把per1的堆内存空间使用权给per2
per1.name = "张三";// 设置per1对象的name属性内容
per1.age = 30; // 设置per1对象的age属性内容
// 设置per2对象的内容,实际上就是设置per1对象的内容
per2.age = 33;
System.out.print("per1对象中的内容 --> ") ;
per1.tell(); // 调用类中的方法
System.out.print("per2对象中的内容 --> ") ;
per2.tell();
}
}
对象的创建
- 变量声明与初始化结合
PlayingCard aCard = new PlayingCard(Diamond, 3);
- 变量声明与创建分离
- 变量的声明只是创建一个标识变量的名称。
- 为了创建对象,程序员必须执行另外的操作。通常这一操作是由new 操作符来表示
对象数组的创建
- 数组的分配和创建
- 数组所包含对象的分配和创建
C++:结合
对象使用缺省构造函数来初始化。
数组由对象组成,每个对象则使用缺省(即无参数)构造函数来进行初始化
PlayingCard *cardArray[52];
Java:new仅创建数组。数组包含的对象必须独立创建。
PlayingCard cardArray[ ] = new PlayingCard[13];
for (int i = 0; i < 13; i++)
cardArray[i] = new PlayingCard(Spade,i+1);
4.内存的分配与回收
指针和内存分配
Java无指针?
- 所有面向对象语言在它们的底层表示中都使用指针,但不是所有的语言都把这种指针暴露程序员。
- 有的时候人们把“Java 语言没有指针”作为Java 和C++语言相比较时的特点,其实更加确切的说法是Java 语言没有程序员可以看到的指针,因为所有的对象引用实际上就是存在于内部表示中的指针。
- 对象引用实际是存在于内部表示中的指针。
- Person x = new Person();//这里就含有一个变量x,没错,这个引用型变量,实质上就是指针
指针问题
- 指针引用堆分配内存。指针变量生存期?
对于堆分配的变量值,只要存在对它的引用,就会一直存在,因此,变量值的生存期一般长于创建该变量过程的生存期。
- 堆分配的内存必须通过某种方式回收。
- 某些语言中,指针和传统变量有区别。
内存回收
- 使用new创建——堆内存
- 堆内存没有绑定在过程的入口和出口处。
- 内存有限
public class ClassDemo06 {
public static void main(String args[]) {
Person per1 = null; // 声明一个per1对象
Person per2 = null; // 声明一个per2对象
per1 = new Person(); // 实例化per1对象
per2 = new Person() ; // 实例化per2对象
per1.name = "张三"; // 设置per1的name属性内容
per1.age = 30; // 设置per1的age属性内容
per2.name = "李四" ; // 设置per2的name属性内容
per2.age = 33 ; // 设置per2的age属性内容
per2 = per1 ; // 将per1的引用传递给per2
System.out.print("per1对象中的内容 --> ") ;
per1.tell(); // 调用类中的方法
System.out.print("per2对象中的内容 --> ") ;
per2.tell();
}
}
内存回收——回收策略
- 在程序中不再使用的对象,将其使用内存回收。
C++:delete
Object Pascal:free
- 垃圾回收机制(Java,C#,Smalltalk)
- 时刻监控对象的操作,对象不再使用时,自动回收其所占内存。
- 通常在内存将要耗尽时工作。
比较
付出额外代价:
- 需要将正在执行的应用程序挂起,回收完成后,程序恢复执行
- 避免释放多次;避免使用已被释放内存;避免耗尽内存。
静态数据字段/静态数据成员
被一个类的所有实例共享的公共数据字段。
Java和C++使用修饰符static创建共享数据字段。
Java:静态数据字段的初始化是在加载类时,执行静态块来完成。
static关键字
1.用static修饰数据成员
利用static关键字修饰的数据成员称为静态数据成员/静态数据字段/静态变量/类成员,也称为静态成员(全局成员)。
static 数据类型 数据成员名;
类名.静态成员;
static不能修饰局部变量!
2.用static修饰成员方法
利用static关键字修饰的成员方法为类方法/静态成员方法,类方法可以由类直接调用。
静态方法与非静态方法、静态数据成员与非静态数据成员之间的调用关系:
结论:静态成员不需要实例化就存在,而非静态成员是实例化后才有的成员,在没有实例化之前非静态成员并不存在。因此可以利用仅仅在某一时刻存在的对象访问普遍存在的对象;而不能用一个普遍存在的对象访问仅仅在某一时刻存在的对象。
类的数据字段(静态数据字段)
对象本身不对共享字段初始化。内存管理器自动将共享数据初始化为某特定值,每个实例去测试该特定值。第一个进行测试的做初始化。
- Java中,静态数据字段的初始化是在加载类时,执行静态块来完成。
- 静态数据字段(静态变量)与静态方法都是在类从磁盘加载至内存后被创建的,与类同时存在,同时消亡。
- 静态变量又称类变量,在类中是全局变量,可以被类中的所有方法调用。
- 只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。 以下是各类型变量初始化值列表:
在类内可以访问所有的static数据成员,采用直接访问方式。
静态成员函数(类方法)
- 成员函数。不能访问非静态成员。
- 无this
- 构造和析构函数不能为静态成员。
静态方法和实例方法
- 使用static修饰的方法称为静态方法
- 使用实例对象调用的方法叫做实例方法
5.构造函数
初始化新创建对象。
- 优点:确保初始化之前不会被使用,防多次调用
当创建和初始化分离时(当使用没有构造函数的编程语言时),程序员在创建新对象之后,很容易忘记调用初始化例程,这样通常会导致不良后果
- Java/C++:名称,返回值
- Java和C#中数据字段可以初始化为特定的数值。
- 构造函数与普通方法的另外一个细微的差异就是构造函数不声明返回值的数据类型
- 构造函数重载:只要每个函数参数的数目、类型或次序不同,就允许多个函数使用相同的名称定义
class Complex { // complex numbers
public Complex (double rv) { realPart = rv; }
public double realPart = 0.0; // initialize data areas
public double imagPart = 0.0; // to zero
}
在构造函数的声明中一定要牢记以下几点:
- 构造方法的名称必须与类名称一致
- 构造方法的声明处不能有任何返回值类型的声明
- 不能在构造方法中使用return返回一个值
class Person {
public Person(){ // 声明构造方法
System.out.println("一个新的Person对象产生。") ;
}
}
public class ConsDemo01 {
public static void main(String args[]) {
System.out.println("声明对象:Person per = null ;") ;
Person per = null ;// 声明对象时不调用构造方法
System.out.println("实例化对象:per = new Person() ;") ;
per = new Person();// 实例化对象时调用构造方法
}
}
运行结果:
构造函数/方法
- 构造方法是隐式调用的。
每次在创建新的对象实例时,会强制执行一次来构造方法
- 构造方法也可以由程序员显式地调用
在子类中调用父类的构造方法
- 构造方法会有的:
缺省构造方法:没有参数的构造方法。如果程序员不提供任何来构造方法,则编译程序自动提供一个构造方法;只要程序员提供了一个构造方法,系统不再提供缺省构造方法
编译程序提供的缺省构造方法只做一件事:调用父类的缺省构造方法
New 语法
Java:
PlayingCard cardSeven = new PlayingCard(); // Java
C++:
PlayingCard *cardEight = new PlayingCard; // C++
final关键字
- 根据程序上下文环境,Java关键字final有“无法改变”或者“终态”的含义
- 它可以修饰非抽象类、非抽象类成员方法和变量
- final类不能被继承,没有子类,final类中的方法默认是final的
- final方法不能被子类的方法覆盖,但可以被继承
- final成员变量表示常量,只能被赋值一次,赋值后值不再改变
- final不能用于修饰构造方法。
final类
- 在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会被扩展,那么就设计为final类。
- 这种类通常我们称为完美类
final方法
- 如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
- 使用final方法的原因有二:
- 把方法锁定,防止任何继承类修改它的意义和实现。
- 高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
final变量(常量)
- 用final修饰的成员变量表示常量,值一旦给定就无法改变
- final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
- 一旦给final变量初值后,值就不能再改变了。
static final int i2 = 99;
- final变量定义的时候,可以先声明,而不给初值,这种变量也称为空白final,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,空白final在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同, 却有保持其恒定不变的特征。
class BlankFinal {
final int i = 0; // Initialized final
final int j; // Blank final
// Blank finals MUST be initialized // in the constructor:
BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet();
}
BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet();
}
public static void main(String[] args) {
BlankFinal bf = new BlankFinal();
}
}
final仅断言相关变量不会赋予新值,并不能阻止在对象内部对变量值进行改变。如对消息的响应。
class Box {
public void setValue (int v);
public int getValue () { return v; }
private int v = 0;
}
final Box aBox = new Box(); // can be assigned only once
aBox.setValue(8); // but can change
aBox.setValue(12); // as often as you like
END