一、类和对象
1、定义类
类是某一批对象的抽象,可以把类理解成某种概念;对象(也被称为实例)才是一个具体存在的实体。定义类的简单语法如下:
[修饰符] class 类名
{
零到多个构造器定义。。。
零到多个成员变量。。。
零到多个方法。。。
}
在上面的语法格式中,修饰符可以是public、final、abstract,或者完全省略这三个修饰符。
注意:对于static来说,static修饰的成员表明它属于这个类本身,而不属于该类的单个实例,因此通常把static修饰的成员变量和方法也称为类变量和类方法。不使用static修饰的普通方法、成员变量则属于该类的单个实例,而不属于该类,因此通常把它们称为实例变量、实例方法。static修饰的成员不能访问没有static修饰的成员。
对于构造器来说,定义格式如下:[修饰符] 构造器名(形参列表)
{
//由零条到多条可执行性语句组成的构造器执行体
}
其中构造器名必须和类名相同。
构造器既不能定义返回值类型,也不能使用viod声明构造器没有返回值。如果为构造器定义了返回值类型,或使用了void修饰,编译时不会出错,但Java会把这个所谓的构造器当成方法来处理——它就不再是构造器。
在Java中如果没有为一个类编写构造器,则系统会为该类提供一个默认的构造器,一旦为一个类提供了构造器,系统将不再为该类提供构造器。
2、对象的产生和使用
创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。
如果访问权限允许,类里定义的方法和成员变量都可以通过类和实例来调用。语法为:类.类变量|方法,或者实例.实例变量|方法。
static修饰的方法和成员变量,既可通过类来调用,也可通过实例来调用;没有使用static修饰的普通方法和成员变量,只可通过实例来调用。
3、对象的this引用
3.1 this关键字总是指向调用该方法的对象。一般有两种情况:
(1)构造器中引用该构造器正在初始化的对象。
(2)在方法中引用调用该方法的对象。
public class Dog
{
// 定义一个jump()方法
public void jump()
{
System.out.println("正在执行jump方法");
}
// 定义一个run()方法,run()方法需要借助jump()方法
public void run()
{
// Dog d = new Dog();
// d.jump();
// 使用this引用调用run()方法的对象
this.jump();
System.out.println("正在执行run方法");
}
}
3.2 Java允许对象的一个成员直接调用另一个成员,可以省略this前缀。也就是说上面的run()方法可以改写成:
public void run()
{
// Dog d = new Dog();
// d.jump();
// 使用this引用调用run()方法的对象
jump();
System.out.println("正在执行run方法");
}
大部分时候,一个方法访问该类中定义的其他方法、成员变量时加不加this前缀的效果是一样的。在static修饰的方法中如果使用this关键字的话,this关键字无法指向合适的对象(因为static修饰方法属于类方法,而this代表的是对象)。所以,static修饰的方法中不能使用this引用。
3.3 Java语法规定:静态成员不能直接访问非静态成员。
public class StaticAccessNonStatic
{
public void info()
{
System.out.println("简单的info方法");
}
public static void main(String[] args)
{
// 因为main()方法是静态方法,而info()是非静态方法,
// 调用main()方法的是该类本身,而不是该类的实例,
// 因此省略的this无法指向有效的对象
info();
}
}
如果确实需要在静态方法中访问另一个普通方法,则只能重新创建一个对象。如上述的info()调用改成:
//创建一个对象作为调用者来调用info()方法
new StaticAccessNonStatic().info();
3.4 大部分时候,普通方法访问其它方法、成员变量时无需使用this前缀,但如果方法里有个局部变量和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用this前缀。同样的在构造器中访问其他成员变量和方法时都可以省略this前缀,但如果构造器中有一个与成员变量同名的局部变量,又必须在构造器中访问这个被覆盖的成员变量,则必须使用this前缀。
public class ThisInConstructor
{
// 定义一个名为foo的成员变量
public int foo;
public ThisInConstructor()
{
// 在构造器里定义一个foo变量
int foo = 0;
// 使用this代表该构造器正在初始化的对象
// 下面的代码将会把该构造器正在初始化的对象的foo成员变量设为6。
this.foo = 6;
}
public static void main(String[] args)
{
// 所有使用ThisInConstructor创建的对象的foo成员变量
// 都将被设为6,所以下面代码将输出6。
System.out.println(new ThisInConstructor().foo);
}
}
二、方法详解
方法在逻辑上要么属于类,要么属于对象。同一个类的一个方法调用另外一个方法时,如果被调方法是普通方法,则默认使用this作为调用者;如果被调用方法是静态方法,则默认使用类作为调用者。
1、方法的参数传递机制
调用方法时实际传给形参的参数值也被称为实参。
Java里方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不收任何影响。
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);
}
}
上述程序执行结果为:
值传递的实质就是:当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋给方法的形参变量,方法里操作的并不是实际的实参变量。Java对于引用类型的参数传递,一样采用的是值传递方式。
2、形参个数可变的方法
如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当做数组传入。
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企业应用实战");
}
}
public static void test(int a,String[] books){}
数组形式的形参可以处于形参列表的任意位置,但个数可变的形参只能处于形参列表的最后。也就是说一个方法中最多只能定义一个长度可变的形参。长度可变的形参本质就是一个数组类型的形参,因此调用包含一个长度可变形参的方法时,这个长度可变的形参既可以传入多个参数,也可以传入一个数组。
如果一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。
方法重载要求的是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如返回值类型、修饰符等,与方法重载没有任何关系。
public class Overload
{
// 下面定义了两个test()方法,但方法的形参列表不同
// 系统可以区分这两个方法,这种被称为方法重载
public void test()
{
System.out.println("无参数");
}
public void test(String msg)
{
System.out.println("重载的test方法 " + msg);
}
public static void main(String[] args)
{
Overload ol = new Overload();
// 调用test()时没有传入参数,因此系统调用上面没有参数的test()方法。
ol.test();
// 调用test()时传入了一个字符串参数,
// 因此系统调用上面带一个字符串参数的test()方法。
ol.test("hello");
}
}
三、成员变量和局部变量
1.1 成员变量指的是在类里定义的变量,局部变量指的是在方法里定义的变量。
其中类变量从该类的准备阶段起开始存在,直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同;而实例变量则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同。
变量分类图如下:
1.2 成员变量无须显式初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化;而局部变量除了形参外,都必须显式初始化。也就是说必须先给定方法局部变量和代码块局部变量指定初始值,否则不能访问它们。
1.3 一个类里面不可以定义两个同名的成员变量,即使一个类变量,一个实例变量也不行;一个方法里不能定义两个同名的方法局部变量,方法局部变量与形参也不能同名;同一个方法中不同代码块内的代码块局部变量可以同名;如果先定义代码块局部变量,后定义方法局部变量,前面定义的代码块变量与后面定义的方法局部变量也可以同名。
Java允许局部变量和成员变量同名,如果方法里的局部变量和成员变量同名,局部变量会覆盖成员变量,如果需要在这个方法里引用被覆盖的成员变量,则可以使用this(对于实例变量)或类名(对于类变量)作为调用者来限定访问成员变量。
2、成员变量与局部变脸的初始化
当系统加载类或创建类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。只有等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。
四、隐藏和封装
封装指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部的信息,而是通过该类所提供的方法来实现内部信息的操作和访问。意义在于:把该隐藏的隐藏起来,把该暴露的暴露出来。
1、访问控制符
private(当前类访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)使用private访问控制符来修饰,则这个成员只能在当前类的内部被访问。
default(包访问权限):如果类里的一个成员(包括成员变量、方法和构造器等)或者一个外部类不使用任何访问控制修饰符,就称它为包访问权限的,default访问控制的成员或外部类可以被相同包下面的其他类访问。
protected(子类访问权限):如果一个成员(包括成员变量、方法和构造器等)使用protected修饰,那么该成员既可以被同一个包中的其他类访问,也可以不同包中的子类访问。
public(公共访问权限):如果一个成员(包括成员变量、方法和构造器等)使用public修饰,那么这个成员或外部类就可以被所有类访问,不管访问类和被访问类是否处于同一个包中,是否具有父子继承关系。
访问控制级别表:
对于局部变量而言,其作用域就是它所在的方法,不可能被其他类访问,因此不能使用访问控制符来修饰。
外部类只能有两种访问控制级别:public和默认,不能使用private和protected修饰。
五、构造器
构造器B包含构造器A的情况:
如果是两个方法存在上述关系,可在方法B中调用方法A。但是构造器不能直接被调用,构造器必须使用new关键字来调用。但一旦使用new关键字来调用构造器,将会导致系统重新创建一个对象。为了能在构造器B中调用构造器A的初始化代码,又不会重新创建一个java对象,可以使用this关键字来调用相应的构造器。
public class Apple
{
public String name;
public String color;
public double weight;
public Apple(){}
// 两个参数的构造器
public Apple(String name , String color)
{
this.name = name;
this.color = color;
}
// 三个参数的构造器
public Apple(String name , String color , double weight)
{
// 通过this调用另一个重载的构造器的初始化代码
this(name , color);
// 下面this引用该构造器正在初始化的Java对象
this.weight = weight;
}
}
程序中this(name,color);调用表明调用该类中另一个带有两个字符串参数的构造器。
使用this调用另一个重载的构造器只能在构造器中使用,而且必须最为构造器执行体的第一条语句。
六、类的继承
1、继承的特点
Java继承是单继承,每个子类只有一个直接父类。通过extends实现继承,父类和子类的关系是一种一般和特殊的关系。语法格式如下:
修饰符 class SubClass extends SuperClass
{
//类定义部分
}
如果一个类没有显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。
public class Bird
{
// Bird类的fly()方法
public void fly()
{
System.out.println("我在天空里自由自在地飞翔...");
}
}
public class Ostrich extends Bird
{
// 重写Bird类的fly()方法
public void fly()
{
System.out.println("我只能在地上奔跑...");
}
public void callOverridedMethod()
{
// 在子类方法中通过super来显式调用父类被覆盖的方法。
super.fly();
}
public static void main(String[] args)
{
// 创建Ostrich对象
Ostrich os = new Ostrich();
// 执行Ostrich对象的fly()方法,将输出"我只能在地上奔跑..."
os.fly();
}
}
上述程序中子类重写了父类的fly()方法。
方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或者相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或者相等;“一大”指的是子类方法的访问权限应该比父类方法的访问权限更大或相等。
覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法一个是实例方法。
如果想要在子类中调用父类被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类被覆盖的方法。
如果父类方法具有private访问权限,则子类无法访问该方法也就不能重写该方法。
父类方法和子类方法之间也可能发生重载,因为子类会获得父类方法,如果子类定义了一个与父类方法有相同的方法名,但是参数列表不同的方法,就会形成父类方法和子类方法的重载
3、super限定
super用于限定该对象调用它从父类继承得到的实例变量或方法。super不能出现在static修饰的方法中。因为static修饰的方法调用者是一个类而不是对象,所以super也就失去了意思,this同理。
如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
class BaseClass
{
public int a = 5;
}
public class SubClass extends BaseClass
{
public int a = 7;
public void accessOwner()
{
System.out.println(a);
}
public void accessBase()
{
// 通过super来限定访问从父类继承得到的a实例变量
System.out.println(super.a);
}
public static void main(String[] args)
{
SubClass sc = new SubClass();
sc.accessOwner(); // 输出7
sc.accessBase(); // 输出5
}
}
在上述代码中,如果在某个方法中访问成员变量a时,没有指定调用者,则系统查找a的顺序如下:
(1)查找该方法中是否有名为a的局部变量;
(2)查找当前类中是否有名为a的成员变量;
(3)查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的成员变量,则系统编译报错。
注意:当程序创建了一个子类对象时,系统不仅会为该类中的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。
4、调用父类构造器
4.1 super和this不能同时出现,因为super和this都必须出现在构造器执行体的第一行。
4.2 不管是否使用super,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器有如下情况:
(1)子类构造器中使用super显式调用父类构造器,系统将根据super传入的实参列表调用父类对应的构造器。
(2)子类构造器使用this显式调用奔雷中的重载构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。
(3)子类构造器中既没有super也没有this时,系统将会在执行子类构造器前隐式调用父类无参构造器。
不管上面哪一种情况,当调用子类构造器来初始化子类对象时,父类构造器总是会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器...一次类推,创建任何对象时,最先执行的总是java.lang.Object类的构造器。
七、多态
1、多态性
Java引用变量的两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定(等号左边),运行时类型由实际赋给该变量的对象决定(等号右边)。
class BaseClass
{
public int book = 6;
public void base()
{
System.out.println("父类的普通方法");
}
public void test()
{
System.out.println("父类的被覆盖的方法");
}
}
public class SubClass extends BaseClass
{
//重新定义一个book实例变量隐藏父类的book实例变量
public String book = "轻量级Java EE企业应用实战";
public void test()
{
System.out.println("子类的覆盖父类的方法");
}
public void sub()
{
System.out.println("子类的普通方法");
}
public static void main(String[] args)
{
// 下面编译时类型和运行时类型完全一样,因此不存在多态
BaseClass bc = new BaseClass();
// 输出 6
System.out.println(bc.book);
// 下面两次调用将执行BaseClass的方法
bc.base();
bc.test();
// 下面编译时类型和运行时类型完全一样,因此不存在多态
SubClass sc = new SubClass();
// 输出"轻量级Java EE企业应用实战"
System.out.println(sc.book);
// 下面调用将执行从父类继承到的base()方法
sc.base();
// 下面调用将执行从当前类的test()方法
sc.test();
// 下面编译时类型和运行时类型不一样,多态发生
BaseClass ploymophicBc = new SubClass();
// 输出6 —— 表明访问的是父类对象的实例变量
System.out.println(ploymophicBc.book);
// 下面调用将执行从父类继承到的base()方法
ploymophicBc.base();
// 下面调用将执行从当前类的test()方法
ploymophicBc.test();
// 因为ploymophicBc的编译类型是BaseClass,
// BaseClass类没有提供sub方法,所以下面代码编译时会出现错误。
// ploymophicBc.sub();
}
}
因为子类是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类的引用变量,无须任何类型转换,或者成为向上转型,向上转型由系统自动完成。例如在上面代码中ploymophicBc引用变量的编译时类型是BaseClass,运行时类型是SubClass,当运行时调用该引用变量的
方法时,其
方法行为总是表现出子类方法的行为特征,而不是父类方法中的行为特征,这就有可能出现:相同类型的变量调用同一个方法时表现出不同的行为特征,这就是多态。与方法不同的是,对象的实例变量不具有多态性。
注意:引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。这句话可以理解为,java有编译和运行两个过程,在编译阶段只能调用编译时类型所具有的方法,正如上面程序中最后被注释掉的一句ploymophicBc.sun(),在编译时就出现错误,那是因为ploymophicBc的编译类型是BaseClass类型,但是在该类中并没有定义sun()方法,所以出错。;
2、引用类型的强制类型转换
在编写java程序时,引用变量只能调用他编译时类型的方法,并不能调用它运行时类型的方法,即使它实际所引用的对象包含该方法(正如上述1、多态性中的代码最后一句)。如果需要调用它运行时类型的方法,则必须把它强制转换成运行时类型。
引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是没有任何继承关系的类型,则编译时报错。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型),否则将在运行时引发ClassCastException异常。
public class ConversionTest
{
public static void main(String[] args)
{
double d = 13.4;
long l = (long)d;
System.out.println(l);
int in = 5;
// 试图把一个数值类型的变量转换为boolean类型,下面代码编译出错
// 编译时候会提示: 不可转换的类型
// boolean b = (boolean)in;
Object obj = "Hello";
// obj变量的编译类型为Object,Object与String存在继承关系,可以强制类型转换
// 而且obj变量实际上类型是String类型,所以运行时也可通过
String objStr = (String)obj;
System.out.println(objStr);
// 定义一个objPri变量,编译类型为Object,实际类型为Integer
Object objPri = new Integer(5);
// objPri变量的编译时类型为Object,objPri的运行时类型为Integer,Object与Integer存在继承关系
// 可以强制类型转换,而objPri变量实际上类型是Integer类型,
// 所以下面代码运行时引发ClassCastException异常
String str = (String)objPri;
}
}
3、instanceof运算符
instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回true,否则返回false。
注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
public class InstanceofTest
{
public static void main(String[] args)
{
// 声明hello时使用Object类,则hello的编译类型是Object,
// Object是所有类的父类, 但hello变量的实际类型是String
Object hello = "Hello";
// String与Object类存在继承关系,可以进行instanceof运算。返回true。
System.out.println("字符串是否是Object类的实例:"
+ (hello instanceof Object));
System.out.println("字符串是否是String类的实例:"
+ (hello instanceof String)); // 返回true。
// Math与Object类存在继承关系,可以进行instanceof运算。返回false。
System.out.println("字符串是否是Math类的实例:"
+ (hello instanceof Math));
// String实现了Comparable接口,所以返回true。
System.out.println("字符串是否是Comparable接口的实例:"
+ (hello instanceof Comparable));
String a = "Hello";
// // String类与Math类没有继承关系,所以下面代码编译无法通过
// System.out.println("字符串是否是Math类的实例:"
// + (a instanceof Math));
}
}
该运算符的作用:在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可以成功转换,从而保证代码更健壮。
1、 使用初始化块
一个类里面可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始块先执行,后面定义的初始块后执行。初始化块的语法格式:
[修饰符]{
//初始化块的可执行性代码
...
}
初始化块的修饰符只能是static,也被称为静态初始化块。
public class Person
{
// 下面定义一个初始化块
{
int a = 6;
if (a > 4)
{
System.out.println("Person初始化块:局部变量a的值大于4");
}
System.out.println("Person的初始化块");
}
// 定义第二个初始化块
{
System.out.println("Person的第二个初始化块");
}
// 定义无参数的构造器
public Person()
{
System.out.println("Person类的无参数构造器");
}
public static void main(String[] args)
{
new Person();
}
}
注意:当Java在创建一个对象时,系统先为该对象的所有实例变量分配内存,接着对这些实例变量进行初始化,顺序为:先执行初始化块或声明实例变量时指定的初始值(这两个地方指定初始值的执行允许与它们在源代码中的排列顺序相同),再执行构造器里指定的初始值。
2、初始化块和构造器
实际上初始化块是一个假象,使用javac命令编译后,该Java类中的初始化块中的代码会被“还原到每一个构造器中,且位于构造器所有代码的前面。”
3、静态初始化块
初始化块如果用static修饰则为静态初始化块,也称为类初始化块,在类初始化阶段就被执行,而不是在创建对象时才执行。因此静态初始化块总是比普通初始化块先执行。
class Root
{
static{
System.out.println("Root的静态初始化块");
}
{
System.out.println("Root的普通初始化块");
}
public Root()
{
System.out.println("Root的无参数的构造器");
}
}
class Mid extends Root
{
static{
System.out.println("Mid的静态初始化块");
}
{
System.out.println("Mid的普通初始化块");
}
public Mid()
{
System.out.println("Mid的无参数的构造器");
}
public Mid(String msg)
{
// 通过this调用同一类中重载的构造器
this();
System.out.println("Mid的带参数构造器,其参数值:"
+ msg);
}
}
class Leaf extends Mid
{
static{
System.out.println("Leaf的静态初始化块");
}
{
System.out.println("Leaf的普通初始化块");
}
public Leaf()
{
// 通过super调用父类中有一个字符串参数的构造器
super("疯狂Java讲义");
System.out.println("执行Leaf的构造器");
}
}
public class Test
{
public static void main(String[] args)
{
new Leaf();
new Leaf();
}
}