文章目录
1 类和对象
1.1 定义类
static修饰的成员不能访问没有static修饰的成员。
如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。 一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。
static的真正作用就是用于区分Field、方法、内部类、初始化块这四种成员到底属于类本身还是属于实例。在类中定义的成员,有static修饰的成员属于类本身,没有static修饰的成员属于该类的实例。
值得指出的是,构造器既不能定义返回值类型,也不能使用void定义构造器没有返回值。如果为构造器定义了返回值类型,或使用void声明构造器没有返回值,编译时不会出错,但Java会把这个所谓的构造器当成方法来处理。
1.2 对象的产生和使用
1.3 对象、引用和指针
1.4 对象的this引用
this可以代表任何对象,当this出现在某个方法体中时,它所代表的对象是不确定的,但它的类型是确定的,它所代表的对象只能是当前类;只有当这个方法被调用
时,它所代表的对象才被确定下来:谁在调用这个方法,this就代表谁。
Java有一个让人极易“混淆”的语法,它允许使用对象来调用static修饰的Field、方法,但实际上这是不应该的
Java编程时不要使用对象去调用static修饰的Field、方法,而是应该使用类去调用static修饰的Field、方法!
普通方法访问其他方法、Field时无须使用this前缀,但如果方法里有个局部变量和Field同名,但程序又需要在该方法里访问这个被覆盖的Field
,则必须使用this
前缀。
除此之外,this引用也可以用于构造器中作为默认引用,由于构造器是直接使用new关键字来调用,而不是使用对象来调用的,所以this在构造器中引用的是该构造器进行初始化的对象
2 方法详解
2.1 方法的所属性
同一个类的一个方法调用另外一个方法时,如果被调方法是普通方法,则默认使用this作为调用者;如果被调方法是静态方法,则默认使用类作为调用者。也就是说,表面上看起来某些方法可以被独立执行,但实际上还是使用this或者类来作为调用者
使用static修饰的方法属于这个类本身,使用static修饰的方法既可以使用类作为调用者来调用,也可以使用对象作为调用者来调用。但值得指出的是,因为使用static修饰的方法还是属于这个类的,因此使用该类的任何对象来调用这个方法时将会得到相同的执行结果,因为实际上还是使用这些实例所属的类作为调用者。
没有static修饰的方法则属于该类的对象,不属于这个类本身。因此没有static修饰的方法只能使用对象作为调用者调用,不能使用类作为调用者调用。使用不同对象作为调用者来调用同一个普通方法,可能得到不同的结果。
2.2 方法的参数传递机制
阅读下面程序,程序输出结果为?
public class PrimitiveTransferTest
{
public static void swap(int a, int b)
{
//下面三行代码实现a、b变量的值交换
//定义一个临时变量来保存a变量的值
int tmp=a;
//把b的值赋给a
a=b;
//把临时变量tmp的值赋给a
b=tmp;
System.out.println("swap方法里,a的值是"
+ a + ";b的值是" + b);
}
public static void main(String[] args)
{
int a=6;
int b=9;
swap(a, b);
System.out.println("交换结束后,变量a的值是"
+ a + ";变量b的值是" + b);
}
}
答:a、b的值没有变,为什么呢?
因为main方法里的变量a和b,并不是swap方法里的a和b。swap方法的a和b只是main方法里变量a和b的复制品
基本类型的参数传递,Java对于引用类型的参数传递,一样采用的是值传递方式
class DataWrap
{
public int a;
public int b;
}
public class ReferenceTransferTest
{
public static void swap(DataWrap dw)
{
//下面三行代码实现dw的a、b两个Field值交换
//定义一个临时变量来保存dw对象的a Field的值
int tmp=dw.a;
//把dw对象的b Field的值赋给a Field
dw.a=dw.b;
//把临时变量tmp的值赋给dw对象的b Field
dw.b=tmp;
System.out.println("swap方法里,a Field的值是"
+ dw.a + ";b Field的值是" + dw.b);
}
public static void main(String[] args)
{
DataWrap dw=new DataWrap();
dw.a=6;
dw.b=9;
swap(dw);
System.out.println("交换结束后,a Field的值是"
+ dw.a + ";b Field的值是" + dw.b);
}
}
执行上面程序,看到如下运行结果:
swap方法里,a Field的值是9;b Field的值是6
交换结束后,a Field的值是9;b Field的值是6
这种参数传递方式是不折不扣的值传递方式,系统一样复制了dw的副本传入swap方法,但关键在于dw只是一个引用变量,所以系统复制了dw变量,但并未复制DataWrap对象。
2.3 形参个数可变的方法
如果在定义方法时,在最后一个形参的类型后增加三点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入
public class Varargs
{
//定义了形参个数可变的方法
public static void test(int a , String... books)
{
//books被当成数组处理
for (String tmp : books)
{
System.out.println(tmp);
}
//输出整数变量a的值
System.out.println(a);
}
public static void main(String[] args)
{
//调用test方法
test(5 , "疯狂Java讲义" , "轻量级Java EE企业应用实战");
}
}
从test的方法体代码来看,形参个数可变的参数其实就是一个数组参数,也就是说,下面两个方法签名的效果完全一样
//以可变个数形参来定义方法
public static void test(int a , String... books);
//下面采用数组形参来定义方法
public static void test(int a , String[] books);
注意:
长度可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个长度可变的形参。调用包含一个长度可变形参的方法时,这个长度可变的形参既可以传入多个参数,也可以传入一个数组。
2.4 方法重载
Java允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。
方法重载的要求就是两同一不同:同一个类
中方法名相同
,参数列表
不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。
为什么方法的返回值类型不能用于区分重载的方法?
答:对于int f(){}和void f(){}两个方法,如果这样调用int result=f();,系统可以识别是调用返回值类型为int的方法;但Java调用方法时可以忽略方法返回值,如果采用如下方法来调用f();,你能判断是调用哪个方法吗?如果你尚且不能判断,那么Java系统也会糊涂。在编程过程中有一条重要规则:不要让系统糊涂,系统一糊涂,肯定就是你错了。因此,Java里不能使用方法返回值类型作为区分方法重载的依据。
不仅如此,如果被重载的方法里包含了长度可变的形参,则需要注意。看下面程序里定义的两个重载的方法。
public class OverloadVarargs
{
public void test(String msg)
{
System.out.println("只有一个字符串参数的test方法 ");
}
//因为前面已经有了一个test方法,test方法里有一个字符串参数
//此处的长度可变形参里不包含一个字符串参数的形式
public void test(String... books)
{
System.out.println("****形参长度可变的test方法****");
}
public static void main(String[] args)
{
OverloadVarargs olv=new OverloadVarargs();
//下面两次调用将执行第二个test方法
olv.test();
olv.test("aa" , "bb");
//下面调用将执行第一个test方法
olv.test("aa");
//下面调用将执行第二个test方法
olv.test(new String[]{"aa"});
}
}
编译、运行上面程序,将看到olv.test();和olv.test(“aa” , “bb”);两次调用的是test(String… books)方法,而olv.test(“aa”);则调用的是test(String msg)方法。通过这个程序可以看出,如果同一个类中定义了test(String… books)方法,同时还定义了一个test(String)方法,则test(String… books)方法的books不可能通过直接传入一个字符串参数,如果只传入一个参数,系统会执行重载的test(String)方法。如果需要调用test(String… books)方法,又只想传入一个字符串参数,则可采用传入字符串数组的形式,如下代码所示。
olv.test(new String[]{"aa"});
大部分时候,我们不推荐重载形参长度可变的方法,因为这样做确实没有太大的意义,而且容易降低程序的可读性。
3 成员变量和局部变量
3.1 成员变量和局部变量
成员变量被分为类Field和实例Field两种,定义Field时没有static修饰的就是实例Field,有static修饰的就是类Field。
提示:正如前面提到的,Java允许通过实例来访问static修饰的Field本身就是一个错误,因此读者以后看到通过实例来访问static Field的情形,都可以将它替换成通过类本身来访问static Field的情形,这样程序的可读性、明确性都会大大提高。
与成员变量不同的是,局部变量除了形参之外,都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们
Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可使用this(对于实例Field)或类名(对于类Field)作为调用者来限定访问成员变量。
3.2 成员变量的初始化和内存中的运行机制
//创建第一个Person对象
Person p1=new Person();
//创建第二个Person对象
Person p2=new Person();
//分别为两个Person对象的name Field赋值
p1.name="张三";
p2.name="孙悟空";
//分别为两个Person对象的eyeNum Field赋值
p1.eyeNum=2;
p2.eyeNum=3;
当程序执行第一行代码Person p1=new Person();时,如果这行代码是第一次使用Person类,则系统通常会在第一次使用Person类时加载这个类,并初始化这个类。
系统接着创建了一个Person对象,并把这个Person对象赋给p1变量,Person对象里包含了名为name的实例Field,实例Field是在创建实例时分配内存空间并指定初始值的
eyeNum类Field并不属于Person对象,它是属于Person类的,所以创建第一个Person对象时并不需要为eyeNum类Field分配内存,系统只是为name实例Field分配了内存空间,并指定默认初始值:null。
3.3 局部变量的初始化和内存中的运行机制
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。
3.4 变量的使用规则
如果仅就程序的运行结果来看,大部分时候都可以直接使用类Field或者实例Field来解决问题,无须使用局部变量。但实际上这种做法相当错误,因为当我们定义一个成员变量时,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或者对象存在范围,这种范围的扩大有两个害处:
增大了变量的生存时间,这将导致更大的内存开销;
扩大了变量的作用域,这不利于提高程序的内聚性。
public class ScopeTest3
{
public static void main(String[] args)
{
//定义一个代码块局部变量作为循环变量
for (int i=0 ; i < 10 ; i++)
{
System.out.println("Hello");
}
}
}
第三个程序最符合软件开发规范:对于一个循环变量而言,只需要它在循环体内有效,因此只需要把这个变量放在循环体内(也就是在代码块内定义),从而保证这个变量的作用域仅在该代码块内。