java基础(三):面向对象(上)
五、面向对象
5.1、类和对象
类的三个最常见成员:构造器、属性、方法;
属性修饰符:public(protected、private)、static、final
方法修饰符:public(protected、private)、static、final(abstract)
1、静态成员和非静态成员
static 修饰的属性、方法称为静态属性、静态方法,属于类共有(类 和 实例 都可以调用,否则只能实例调用)。
静态成员(属于类共有,类调用时没有创建实例)不能直接访问非静态成员(非静态成员必须使用实例来调用,如main方法要new对象);
2、对象的产生、引用和指针
Person p = new Person();
创建对象:通过new关键字调用Person类的构造器,返回一个Person实例(创建一个对象)并赋值给p变量;
JAVA除了8种基本数据类型,还有引用类型:类、数组、接口、null类型;
上面代码产生两个实体:p变量、Person对象。因为类是引用数据类型,所以Person对象赋值给p变量时只是把对象的引用赋值给了p(存放在栈内存中),真正的Person对象则是存放在堆内存中。
Person p2 = p; //p引用变量只存放了对象的引用地址,这里只是把p保存的地址赋给了p2
3、对象的this引用
this关键字总是指向调用该方法的 对象(实例),它的最大作用是:让类中的一个方法,调用类中的另一个方法或属性。
static修饰的方法中不能使用 this,因为它是静态成员(属于类共有,类调用时没有创建实例),无法指向合适的对象(静态成员不能直接访问非静态成员)。
public class Dog{ public void jump(){ System.out.println("执行jump方法"); } public void run(){ this.jump(); //jump(); //java允许一个对象直接调用另一个对象,一般情况下this可以省略 System.out.println("执行run方法"); } }
this与super的区别是什么?
this 总是指向调用该方法的 对象(实例),它的最大作用是:让类中的一个方法,调用类中的另一个方法或属性。
this的用法在java中大体可以分为3种:
1.普通的直接引用,this相当于是指向当前对象本身。
2.形参与成员名字重名,用this来区分,即如果方法(构造器)里有一个局部变量和属性同名,但要在方法里访问这个被覆盖的属性,则必须用this前缀。
3.引用本类的构造函数。
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super也有三种用法:
1.普通的直接引用:与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分;
3.引用父类构造函数:
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
super() 和 this() 的区别是:
super() 在子类中调用父类的构造方法,this() 在本类内调用本类的其它构造方法。
super() 和 this() 均需放在构造方法内第一行。
尽管可以用 this 调用一个构造器,但却不能调用两个。
this 和 super 不能同时出现在一个构造函数里面。因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,
所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量、static方法、static语句块。
从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个Java关键字。
5.2、 方法详解
Java语言里,方法不能独立存在,方法必须属于类或对象。一旦将一个方法定义在某个类的类体内,如果这个方法使用了static 修饰,则这个方法属于这个类,否则这个方法属于这个类的 实例。
Java里方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。
形参长度可变的方法:从JDK 1.5之后,Java允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。
注意事项:
1、一个方法中最多 只能包含一个 个数可变的形参;
2、个数可变的形参 只能处于形参列表的最后;
3、个数可变的形参 本质就是一个数组类型,因此调用时,该形参既可以传入多个参数,也可以传入一个数组。
public static void test(int a, String ... books); //以可变个数形参定义方法,只能有一个,只能处于形参列表最后,传值时可以传数组 public static void test(int a, String[] books); //效果同上 test(5, "语文", "数学"); //可变个数形参方法赋值,调用方法时根据简洁 test(5, new String[]{"语文", "数学"}); //赋值,可用于上面两个方法
方法递归:一个方法体内调用它自身,被称为方法递归。只要一个方法的方法体实现中再次调用了方法本身,就是递归方法。递归一定要向已知方向递归。
方法重载:如果同一个类中包含了两个或两个以上方法的 方法名相同,但形参列表不同,则被称为方法重载。
/** * 递归方法:一个方法体内调用自身,向已知方向递归 * 示例1:已知一个数列:f(0)=1, f(1)=4, f(n+2)=2*f(n+1) + f(n),其中n是大于0的整数,求f(10) * 分析:f(0)、f(1) 已知,求f(10),向小的方向递归,通用公式变为:f(n)=2*f(n-1) + f(n-2) * 即将 n = n-2,使得向f(1)、f(0)方向递归(向已知方向递归) */ public static int fn(int n) { if(n==0) { return 1; } else if(n==1) { return 4; } else { return 2 * fn(n-1) + fn(n-2); //方法中调用它自身,就是方法递归 } } /** * 递归方法:一个方法体内调用自身,向已知方向递归 * 示例2:已知一个数列:f(20)=1, f(21)=4, f(n+2)=2*f(n+1) + f(n),其中n是大于0的整数,求f(10) * 分析:f(20)、f(21) 已知,求f(10),向大的方向递归,通用公式变为:f(n)=f(n+2) - 2*f(n+1) * 向f(20)、f(21)方向递归(向已知方向递归) */ public static int fn2(int n) { if(n==20) { return 1; } else if(n==21) { return 4; } else { return fn2(n+2) - 2 * fn2(n+1); //方法中调用它自身,就是方法递归 } } //方法重载 public void test(String str) { System.out.println("只有一个字符串的test方法"); } public void test(String ... books) { System.out.println("形参长度可变的test方法"); } public static void main(String[] args) { //递归方法测试 System.out.println(fn(10)); //输出fn(10)的结果为: 10497 System.out.println(fn2(10)); //输出fn2(10)的结果为: -3771 TaskEveryTenMin t = new TaskEveryTenMin(); //执行第一个test方法,打印出:只有一个字符串的test方法 t.test("aa"); //执行第二个test方法,下面三个都打印出:形参长度可变的test方法 t.test(); t.test("aa", "bb"); t.test(new String[] {"aa"}); //想调用第二个方法,又想只传一个字符串参数则调用这个方法,可传一个字符串数组 }
5.3、 成员变量和局部变量
成员变量:指的是在 类里定义的变量;
局部变量:指的是在 方法里定义的变量。
变量名称建议:第一个单词首字母小写,后面每个单词首字母大写。Java程序中的变量划分如下图所示:
1、成员变量 无须显式初始化。系统 会在 加载类 或 创建该类的实例 时,自动为成员变量分配内存空间,自动为成员变量指定初始值。
2、局部变量 除形参之外,都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。
3、局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,
系统才会为局部变量分配内存,并将初始值保存到这块内存中。与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。
如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,
通过该地址引用到该变量实际引用的对象或数组。栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。
5.4、 隐藏和封装
封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都需要通过使用Java提供的访问控制符来实现。
Java提供了4个访问控制符:private、default、protected 、public,提供了4个访问控制级别。Java的访问控制级别由小到大如下图所示:
关于访问控制符的使用,存在如下几条基本原则:
1、类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量,才可能考虑使用public修饰。
除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。
2、 如果某个类主要用做其他类的父类,该类里的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。
3、希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通常使用public修饰,从而允许在其他地方创建该类的实例。
4、package 语句必须作为源文件的第一条非注释性语句,一个源文件只能指定一个包,即只能包含一条package语句,
该源文件中可以定义多个类,则这些类将全部位于该包下。如果没有显式指定package语句,则处于默认包下。
5、import 可以向某个Java文件中导入指定包层次下某个类或全部类,import语句应该出现在package语句(如果有的话)之后、类定义之前。
6、JDK 1.5以后更是增加了一种静态导入的语法,它用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法。
静态导入使用 import static 语句,静态导入也有两种语法,分别用于导入指定类的单个静态成员变量、方法和全部静态成员变量、方法。
Java的核心类都放在java包以及其子包下,Java扩展的许多类都放在javax包以及其子包下。
下面几个包是Java语言中的常用包:
java.lang:这个包下包含了Java语言的核心类,如String、Math、System和Thread类等,使用这个包下的类无须使用import语句导入,系统会自动导入这个包下的所有类。
java.util:这个包下包含了Java的大量工具类/接口和集合框架类/接口,例如Arrays和List、Set等。
java.net:这个包下包含了一些Java网络编程相关的类/接口。
java.io:这个包下包含了一些Java输入/输出编程相关的类/接口。
java.text:这个包下包含了一些Java格式化相关的类。
java.sql:这个包下包含了Java进行JDBC数据库编程的相关类/接口。
java.awt:这个包下包含了抽象窗口工具集的相关类/接口,这些类主要用于构建图形用户界面(GUI)程序。
java.swing:这个包下包含了Swing图形用户界面编程的相关类/接口,这些类可用于构建平台无关的GUI程序。
5.5、 深入构造器
构造器最大的用处就是在创建对象时执行初始化。同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为 构造器重载。
使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器。
5.6、 类的继承
Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称其为基类、超类。父类和子类的关系,是一种一般和特殊的关系。
注意:子类只能从父类获得 成员变量、方法和内部类(包括内部接口、枚举),不能获得 构造器和初始化块。
如果定义一个Java类未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,java.lang.Object类 是所有类的父类,要么是其直接父类,要么是其间接父类。
方法重写:子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
方法的重写要遵循“两同两小一大”规则:
“两同”即方法名相同、形参列表相同;
“两小”指的是子类方法 返回值类型 应比父类方法返回值类型更小或相等,子类方法声明 抛出的异常类 应比父类方法声明抛出的异常类更小或相等;
“一大”指的是子类方法的 访问权限 应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。
如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。
如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。
方法重载:主要发生在 同一个类的 多个 同名方法之间,而重写发生在 子类和父类 的 同名方法 之间。
super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。
5.7、 多态
Java引用变量有两个类型:一个是 编译时类型,一个是 运行时类型。
编译时类型 由声明该变量时使用的类型决定,运行时类型 由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。
多态:相同类型的变量 调用同一个方法时呈现出 多种不同的行为特征,这就是多态。与方法不同的是,对象的实例变量则不具备多态性。
instanceof 运算符的前一个操作数通常是一个引用类型变量,后一个通常是一个类(或接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。是则返回true,否则false。
在使用instanceof运算符时需要注意:instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。
5.8 、继承与组合
继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。
子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接复用父类的成员变量和方法。
为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:
1、尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量。
2、不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;
如果父类中的方法需要被外部类调用,则必须以 public 修饰,但又不希望子类重写该方法,可以使用 final 修饰符来修饰该方法;
如果希望父类的某个方法 被子类重写,但 不希望被其他类自由访问,则可以使用 protected 来修饰该方法。
3、尽量不要在父类构造器中调用将要被子类重写的方法。
4、如果想把某些类设置成最终类,即不能被当成父类,则可以使用 final 修饰这个类,例如JDK提供的 java.lang.String 类 和 java.lang.System 类。
5、除此之外,使用 private 修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有的构造器都使用private修饰的父类而言,可另外提供一个静态方法,用于创建该类的实例。
6、如果需要复用一个类,除把这个类当成基类来继承之外,还可以把该类当成另一个类的组合成分,从而允许新类直接复用该类的public方法。
5.9、 初始化块
一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。
在 Java 语言中,当实例化对象时,对象所在类的所有 成员变量 首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建象。
初始化一般遵循3个原则:
1、静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
2、父类优先于子类进行初始化;
3、按照 成员变量的定义顺序 进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;
加载顺序:
父类(静态变量、静态语句块)
子类(静态变量、静态语句块)
父类(实例变量、普通语句块)
父类(构造函数)
子类(实例变量、普通语句块)
子类(构造函数)