第五章.面向对象(上)
类:可被认为是一种自定义的数据类型,可使用类来定义变量,所有使用类定义的变量都是引用变量,所有的类是引用类型。
Java程序使用类的构造器来创建该类的对象。
Java支持面向对象的三大特征:封装、继承、多态:
java提供了private、protected、public访问控制修饰符来实现封装,提供extends关键字让子类继承父类,有了继承就有了多态。
构造器用于对类的实例进行初始化操作,构造器支持重载,若多个重载构造器里包含了相同的初始化代码,可以把这些初始化代码放置在普通初始化块里完成,初始化块总是在构造器
执行前被调用。
静态初始化块:用于初始化类,在类的初始化阶段被执行。若继承书里的某个类需要被初始化时,系统将会同时初始化该类的所有父类。
类包含三种常见的成员:
1.构造器
通过new关键字来调用构造器,返回该类的实例。
一个类没有构造器,该类无法创建实例。所以Java语言默认功能:若程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器;一旦提供了构造器,系统将
不再为该类提供构造器。
修饰符可以是public、protected、private(三选一),也可以省略
构造器名必须和类名相同,构造器既不能定义返回值类型,也不能使用void声明构造器没有返回值。若构造器有了返回值类型或void,编译不会出错,但Java会把这个
所谓的构造器当成方法来处理——它就不是构造器了
不要在构造器中显示使用return返回当前类的对象,因为构造器中的返回值是隐式的。
系统默认构造器是没有参数的。
2.成员变量
定义该类或该类的实例所包含的状态数据
修饰符可以是public、protected、private(三选一),static、final,也可以省略
3.方法
定义该类或该类的实例的行为特征或功能实现
修饰符可以是public、protected、private(三选一),final、abstract(二选一),static,也可以省略
static修饰的成员不能访问没有static修饰的成员。static是一个特殊关键字,它修饰的成员表明成员属于这个类本身,不属于该类的单个实例。通常把static修饰的成员变量
和方法称为类变量、类方法。
static真正作用是用于区分成员变量、方法、内部类、初始化块这四种成员是属于类本身还是属于实例。
Java类的作用:以Person类为例
1.定义变量
Person p;
2.创建对象
p = new Person();
3.调用类的类方法或访问类的类变量
p.name = "lanshanxiao";
p.say("Java 学习很简单,很容易!");
对象的this引用:
this关键字总是指向调用该方法的对象。根据this出现的位置不同,this作为对象的默认引用有两种情形:
1.构造器中引用 该构造器正在初始化的对象
2.在方法中引用 调用该方法的对象
this关键字最大的作用就是让类中一个方法,可以访问该类中的另一个方法或实例变量
this可以代表任何对象,当this出现在某个方法体中时,它所代表的对象是不确定的,但它的类型是确定的:它所代表的只能是当前类的实例;只有当这个方法被调用时,它所
代表的对象才能被确定下来:谁在调用这个方法,this就代表谁。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Dog{ 2 //定义一个jump()方法 3 public void jump(){ 4 System.out.println("正在执行jump方法"); 5 } 6 7 //定义一个run()方法,run()方法需要借助jump()方法 8 public void run(){ 9 //使用this引用run()方法的对象 10 this.jump(); 11 System.out.println("正在执行run方法"); 12 } 13 } 14 15 public class DogTest{ 16 public static void main(String[] args){ 17 //创建一个Dog对象 18 Dog dog = new Dog(); 19 //调用dog对象的run方法 20 dog.run(); 21 } 22 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Dog{ 2 //定义一个jump()方法 3 public void jump(){ 4 System.out.println("正在执行jump方法"); 5 } 6 7 //定义一个run()方法,run()方法需要借助jump()方法 8 public void run(){ 9 //调用自身jump()方法 10 jump(); 11 System.out.println("正在执行run方法"); 12 } 13 } 14 15 public class DogTest{ 16 public static void main(String[] args){ 17 //创建一个Dog对象 18 Dog dog = new Dog(); 19 //调用dog对象的run方法 20 dog.run(); 21 } 22 }
这说明从自身方法调用本身的另一个方法时,this是可以省略的。但这只是假象,这个程序省略的this依然存在。
对于static修饰的方法而言,可以使用类名直接调用该方法,如果在static修饰的方法中使用this关键字,则this就无法指向合适的对象。所以static修饰的方法中不能使用this引用
由于static修饰的方法不能使用this引用,所以static修饰的方法不能访问不使用static修饰的普通成员,因此,Java语法规定:静态成员不能直接访问非静态成员。
下面是一个静态方法调用了非静态方法的例子:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class StaticAccessNonStatic{ 2 public void info(){ 3 System.out.println("简单的info方法"); 4 } 5 6 public static void main(String[] args){ 7 //因为main()方法时静态方法,而info()非静态方法 8 //调用main()方法的是该类本身,而不是该类的实例 9 //因此省略的this无法指向有效的对象 10 info(); 11 } 12 }
这里要提醒一句:
Java编程中不要使用对象去调用static修饰的成员变量、方法。虽然可以使用对象调用static修饰的成员变量、方法,但是最好不要使用,当我们遇到了对象调用static修饰的
成员变量、方法时,我们去修改成用类名调用static修饰的成员变量、方法。因为static修饰的成员变量、方法属于类本身,不属于任何实例。
若确实需要在静态方法中访问另一个普通方法,则只能重新创建一个对象:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class StaticAccessNonStatic{ 2 public void info(){ 3 System.out.println("简单的info方法"); 4 } 5 6 public static void main(String[] args){ 7 //创建一个对象作为调用者来调用info()方法 8 new StaticAccessNonStatic().info(); 9 } 10 }
this也可以作为普通方法的返回值:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class ReturnThis{ 2 public int age; 3 public ReturnThis grow(){ 4 age++; 5 //return this返回调用该方法的对象 6 return this; 7 } 8 9 public static void main(String[] args){ 10 ReturnThis rt = new ReturnThis(); 11 //可以连续调用同一个方法 12 rt.grow() 13 .grow() 14 .grow(); 15 System.out.println("rt的age成员变量值是:" + rt.age); 16 } 17 }
使用this作为方法的返回值可以让代码更加简洁,但可能造成实际意义的模糊。
方法:
Java语言中,方法不能独立存在,方法必须属于类或对象。
一旦将一个方法定义在某个类的体内,若方法使用了static修饰,则方法属于类,否则方法属于类的实例。
执行方法时必须使用类名或对象作为调用者。
同一个类的一个方法调用另一个方法时,若被调方法是普通方法,则默认使用this作为调用者;若被调方法是静态方法,则默认使用类名作为调用者。
使用static修饰的方法属于类本身,因此使用该类任何对象来调用这个方法时,将会得到相同的执行结果;没有static修饰的方法则属于该类的对象,不属于类本身,因此
使用不同对象作为调用者来调用同一个普通方法,可能得到不同的结果。
形参个数可变的方法:
若在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Varargs{ 2 //定义了一个参数个数可变的方法 3 //和public static void test(int a, String[] books)等价 4 public static void test(int a, String ... books){ 5 //books被当成数组处理 6 for(String tmp : books){ 7 System.out.println(tmp); 8 } 9 //输出整数变量a的值 10 System.out.println(a); 11 } 12 13 public static void main(String[] args){ 14 //调用test方法 15 test(5, "疯狂Java讲义", "轻量级Java EE企业应用实战"); 16 test(5, new String[] {"疯狂Android讲义", "经典Java EE企业应用实战"}); 17 } 18 }
个数可变的形参只能处于形参列表的最后,也就是说,一个方法中最多只能有一个个数可变的形参。
个数可变的形参本质就是一个数组类型的形参,因此调用包含个数可变形参的方法时,该个数可变的形参既可以传入多个参数,也可以传入一个数组。
方法重载:
方法重载要求两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法的返回值类型、修饰符等,与方法重载没有任何关系。
成员变量和局部变量:
成员变量:
A:在类方法外
B:在堆内存中
C:有默认的初始化值
局部变量:
A:在方法定义中
B:在栈内存中
C:随着方法的调用而存在,随着方法的调用完毕而销毁
D:除了形参之外的局部变量没有初始化的值,使用前必须定义和赋值
E:定义局部变量后,系统并没有为这个变量分配内存空间,知道程序为这个变量赋初始值,才会为局部变量分配内存空间。
Java允许局部变量和成员变量同名,若方法中的局部变量和成员变量同名,局部变量会覆盖成员变量,若需要在这个方法中引用被覆盖的成员变量,可以使用this(对于
实例变量)或类名(对于类变量)作为调用者来限定访问成员变量:
public void variable(String name){
this.name = name;//成员变量this.name,局部变量name
}
隐藏和封装:
封装是面向对象的三大特征之一,它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作
和访问。
访问控制级别:
private:(当前类可以访问)
default:(包访问权限)default访问控制的成员或外部类可以被相同包下的其他类访问
protected:(子类访问权限)可被不同包下的子类访问,通常使用protected修饰一个方法,通常是希望其子类来重写这个方法
public:(公共访问权限)
若一个Java源文件中定义的所有类都没有使用public修饰,则这个Java源文件的文件名可以是一切合法的文件名;但若一个Java源文件中定义了一个public修饰的类,
则这个源文件的文件名必须与public修饰的类的类名相同。
package、import和 import static:
package:提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。
java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package lanshanxiao; 2 3 public class Hello{ 4 public static void main(String[] args){ 5 System.out.println("Hello World!"); 6 } 7 }
自动产生一个lanshanxiao文件夹:
一个Java源文件只能有一个package语句,但是可以有多个import语句。
import com.lanshanxiao.*;//星号代表只导入com.lanshanxiao包下的所有类,但是不能导入com.lanshanxiao包下的子包
import com.lanshanxiao.first.*;//同样只能导入com.lanshanxiao.first包下的所有类。
Java默认为所有源文件导入java.lang包下所有类。
静态导入(import static):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 import static java.lang.System.*; 2 import static java.lang.Math.*; 3 //import和import static的区别 4 //import可以省略写包名 5 //import static 连类名都省略 6 //import static java.lang.System.*;//导入指定类的全部静态变量成员和静态方法,这里导入了System类下所有静态成员变量和静态方法 7 //import static java.lang.System.out;//导入指定类的单个静态成员变量和静态方法,这里导入了out静态成员变量 8 9 public class StaticImportTest{ 10 public static void main(String[] args){ 11 //out是java.lang.System类的静态成员变量,代表标准输出 12 //PI是java.lang.Math类的静态成员变量,表示圆周率常量 13 out.println(PI); 14 //直接调用Math类的sqrt静态方法 15 out.println(sqrt(256)); 16 } 17 }
构造器:this();
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class Apple{ 2 public String name; 3 public String color; 4 public double weight; 5 6 public Apple(){} 7 //两个参数的构造器 8 public Apple(String name, String color){ 9 this.name = name; 10 this.color = color; 11 } 12 13 //三个参数的构造器 14 public Apple(String name, String color, double weight){ 15 //通过this调用另一个重载的构造器的初始化代码 16 this(name, color); 17 //下面this引用该构造器正在初始化的Apple对象 18 this.weight = weight; 19 } 20 } 21 22 //程序中this(name, color);该语句只能在构造其中使用,而且必须作为构造器执行体的第一条语句。
为了防止代码重复一般构造其中调用另一构造器的方法就是 this();
类的继承:
关键在extends;
方法重写@Override
遵循“两同两小一大”规则,“两同”:方法名相同、形参列表相同;“两小”:子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比
父类方法声明跑出的异常类更小或相等;“一大”:子类方法的访问权限应比父类方法访问权限更大或相等。
覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //错误的方法覆盖 2 class BaseClass{ 3 public static void test(){} 4 } 5 6 class SubClass{ 7 public void test(){} 8 }
子类覆盖了父类同名方法,若想访问父类同名方法可以使用super.方法名()(被覆盖的是实例方法)访问父类同名方法 或 父类名.方法名()(被覆盖的是类方法)访问
父类同名方法。
若父类方法具有private访问权限,则子类不能继承该方法。也无法重写该方法。若子类中定义了一个与父类private方法同名的方法名、相同的形参列表、相同的返回值类型
的方法,依然不是重写,只是在子类中重新定义了一个新方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class BaseClass{ 2 //test()方法是private,子类不能继承 3 private void test(){} 4 } 5 6 class SubClass{ 7 //不是方法重写,所以可以增加static关键字 8 public static void test(){} 9 }
super关键字:
用于限定该对象调用它从父类继承得到的实例变量或方法。
super也不能和static修饰符一起使用(和this一样)
在构造器中使用super(),用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Parent { 2 public String tag = "疯狂Java讲义"; 3 } 4 5 class Derived extends Parent{ 6 //定义一个私有的tag实例变量来隐藏弗雷德tag实例变量 7 private String tag = "轻量级Java EE企业应用实战"; 8 } 9 10 public class HideTest{ 11 public static void main(String[] args){ 12 Derived d = new Derived(); 13 //程序不可访问d的私有变量tag,所以下面语句将引起编译错误 14 System.out.println(d.tag); 15 //将d变量显式的向上转型为Parent后,即可访问tag实例变量 16 System.out.println(((Parent) d).tag); 17 } 18 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Parent { 2 public String tag = "疯狂Java讲义"; 3 } 4 5 class Derived extends Parent{ 6 //定义一个私有的tag实例变量来隐藏弗雷德tag实例变量 7 private String tag = "轻量级Java EE企业应用实战"; 8 } 9 10 public class HideTest{ 11 public static void main(String[] args){ 12 Derived d = new Derived(); 13 //程序不可访问d的私有变量tag,所以下面语句将引起编译错误 14 //System.out.println(d.tag); 15 //将d变量显式的向上转型为Parent后,即可访问tag实例变量 16 System.out.println(((Parent) d).tag); 17 } 18 }
使用在子类构造器中super()调用父类构造器,必须在子类构造器中的第一句(和this()一样)。
不管是否显式使用super(),子类构造器总会调用父类构造器一次。
通过一个this()和super()的例子,看一下运行结果,想一想为什么会这样?
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Creature{ 2 public Creature(){ 3 System.out.println("Creature无参数的构造器"); 4 } 5 } 6 7 class Animal extends Creature{ 8 public Animal(String name){ 9 System.out.println("Animal 带一个参数的构造器,该动物的name为:" + name); 10 } 11 12 public Animal(String name, int age){ 13 //使用this调用同一个重载的构造器 14 this(name); 15 System.out.println("Animal 带两个参数的构造器,该动物的age为:" + age); 16 } 17 } 18 19 public class Wolf extends Animal{ 20 public Wolf(){ 21 //显示调用父类有两个参数的构造器 22 super("灰太狼", 3); 23 System.out.println("Wolf 无参数的构造器"); 24 } 25 26 public static void main(String[] args){ 27 new Wolf(); 28 } 29 }
多态:
子类继承父类,并重写父类的方法,这样方法就会有了多态;但是实例成员变量不会有多态,引用类型是谁,则实例变量就是谁的;一旦使用多态(向上转型),那么
父类中有的方法,子类才可以调用(编译看左边,运行看右边):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class BaseClass{ 2 public int book = 5; 3 public void test(){ 4 System.out.println("父类被覆盖的方法"); 5 } 6 } 7 8 class SubClass extends BaseClass{ 9 public String book = "疯狂Java讲义"; 10 11 public void test(){ 12 System.out.println("子类覆盖父类方法"); 13 } 14 15 public void sub(){ 16 System.out.println("子类普通方法"); 17 } 18 public static void main(String[] args){ 19 BaseClass bc = new SubClass(); 20 System.out.println(bc.book); 21 bc.test(); 22 23 //因为bc编译时的类型是BaseClass 24 //BaseClass类中没有提供sub()方法,所以下面代码编译时会出错 25 bc.sub(); 26 } 27 }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class BaseClass{ 2 public int book = 5; 3 public void test(){ 4 System.out.println("父类被覆盖的方法"); 5 } 6 } 7 8 class SubClass extends BaseClass{ 9 public String book = "疯狂Java讲义"; 10 11 public void test(){ 12 System.out.println("子类覆盖父类方法"); 13 } 14 15 public void sub(){ 16 System.out.println("子类普通方法"); 17 } 18 public static void main(String[] args){ 19 BaseClass bc = new SubClass(); 20 System.out.println(bc.book); 21 bc.test(); 22 23 //因为bc编译时的类型是BaseClass 24 //BaseClass类中没有提供sub()方法,所以下面代码编译时会出错 25 //bc.sub(); 26 } 27 }
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法。
通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量。
强制类型转换:
1.基本类型之间的转换只能在数值类型之间进行,数值类型包括整型、字符型、浮点型。但数值类型和布尔类型之间是不能进行类型转换的
2.引用类型之间的转换只能在具有继承关系的两个类型之间进行,否则编译会报错。若试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才可以(即
编译时类型为父类类型,而运行时类型是子类类型),否则将在运行时引发ClassCastException异常。
3.考虑到进行强制类型转换可能出现异常,因此进行类型转换之前先通过instanceof运算符来判断是否可以成功转换。
instanceof运算符:
前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是一个接口)
注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
继承是实现类复用的重要手段,但继承带来了一个最大的坏处就是:破坏封装。
组合:采用组合方式来实现类复用则能提供更好的封装性。
父类构造器中调用了被其子类重写的方法,则调用的是被子类重写后的方法:http://www.cnblogs.com/chuliang/p/5813641.html
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Base{ 2 public Base(){ 3 test(); 4 } 5 6 public void test(){ 7 System.out.println("将被子类重写的方法"); 8 } 9 } 10 11 public class Sub extends Base{ 12 private String name; 13 public void test(){ 14 System.out.println("子类重写父类的方法,其name字符串长度:" + name.length()); 15 } 16 17 public static void main(String[] args){ 18 //下面的代码会引发空指针异常 19 Sub s = new Sub(); 20 } 21 }
静态初始化块,普通初始化块,构造器,它们执行的顺序用一个Java程序来看:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 class Root{ 2 static{ 3 System.out.println("Root的静态初始化块"); 4 } 5 6 { 7 System.out.println("Root的普通初始化块"); 8 } 9 10 public Root(){ 11 System.out.println("Root的无参数构造器"); 12 } 13 } 14 15 class Mid extends Root{ 16 static{ 17 System.out.println("Mid的静态初始化块"); 18 } 19 20 { 21 System.out.println("Mid的普通初始化块"); 22 } 23 24 public Mid(){ 25 System.out.println("Mid的无参数构造器"); 26 } 27 28 public Mid(String msg){ 29 //通过this调用同一类中重载的构造器 30 this(); 31 System.out.println("Mid的带参数构造器,其参数值:" + msg); 32 } 33 } 34 35 class Leaf extends Mid{ 36 static{ 37 System.out.println("Root的静态初始化块"); 38 } 39 40 { 41 System.out.println("Root的普通初始化块"); 42 } 43 44 public Leaf(){ 45 //通过super()调用弗雷中有一个字符串参数的构造器 46 super("疯狂Java讲义"); 47 System.out.println("执行Leaf的构造器"); 48 } 49 } 50 51 public class Test { 52 public static void main(String[] args){ 53 new Leaf(); 54 new Leaf(); 55 } 56 }
第一次new Leaf()和第二次new Leaf()结果是不同的。看上面的结果想一想为什么?
GitHub链接:https://github.com/lanshanxiao/-Java-