第5章 面向对象(上)
5.1 类和对象
类可以当成是一种自定义类型,可以使用类来定义变量。这种类型变量称为引用变量。
5.1.1 定义类
[修饰符] class 类名
{
构造器
成员变量
方法
}
各成员之间的定义顺序没有任何影响,各成员之间可以相互调用。
@static成员不能调用非static 成员。
成员变量用于定义类包含的状态数据,方法用于定义类的行为或功能实现。构造器用于构造类的实例,java通过new调用构造器,返回类的实例。
定义成员变量的语法:
[修饰符] 类型 成员变量名 [=默认值];
定义方法的语法:
[修饰符] 返回值类型 方法名(形参列表)
{//方法体}
定义构造器的语法
[修饰符] 构造器名(形参列表)
{}
构造器名必须和类名相同
@static 是一个特殊的关键字,static修饰的成员表明它属于这个类本身,而不属于类的单个实例。
定义一个Person类:
public class Person
{
public String name;
public int age;
public void say(String content){
System.out.println(content);
}
}
Person没有定义构造器,系统为它提供默认构造器。
5.1.2 对象的产生和使用
创建对象的途径是构造器,通过new即可调用构造器来创建实例。
Person p;
p = new Person();
@可以简写为 Person p = new Person();
使用对象:
1、访问对象的实例变量
2、调用对象的方法
p.name = “李刚”;
p.say(“Java很简单”);
@static修饰的方法和变量,既可以通过类来调用,也可以通过实例来调用。
5.1.3 对象、引用和指针
和数组类似,类也是一种引用类型。
Person p2 = p; 其实是将p的地址赋值给p2,p2和p指向的是同一个Person对象。
5.1.4 对象的this引用
this关键字总是指向调用该方法的对象。
this的使用有两种情形:
1、构造器中引用该构造器正在初始化的对象
2、在方法中引用调用该方法的对象。
this最大的作用就是让类中的一个方法,访问该类的另一个方法或实例变量。
假设有一个Dog类
Dog类有jump()方法,现在定义一个run()方法,要在run()方法中调用jump()
public void run(){
this,jump();
}
@java允许对象的一个成员直接调用另一个成员,可以省略this前缀
当方法中的某个局部变量和成员变量同名时,要使用被覆盖的成员变量,就要使用this前缀。
public Test{
public int foo;
public Test{
int foo = 0;
this.foo = 6;
}
}
5.2 方法详解
方法是类或对象的行为特征的抽象。从功能上看,方法完全类似于传统结构化程序设计中的函数。
5.2.1 方法的所属性
方法是类和对象的附属。体现在:
1、方法不能独立定义,只能在类中定义
2、从逻辑上看,方法要么属于类本身,要么属于类的一个对象
3、永远不能独立执行方法,执行方法必须使用类或对象作为调用者。
5.2.2 方法的参数传递机制
Java方法的参数传递方式只有一种,值传递。 //将实际参数值的副本传入方法内,参数本身不受影响
引用类型传递仍然是值传递,但传递的是引用的副本,通过操作副本来访问对象也会改变对象本身。
(因为对象就是在堆内存里的那一个对象,并没有复制另一个对象,复制的是引用变量)
5.2.3 形参个数可变的方法
定义方法时,在最后一个形参的类型后面加三点…,表明该形参可以接收多个参数值。多个参数值被当成数组传入。
public class Varargs
{
public static void test(int a, String… books)
{
for(String tmp : books)
{
System.out.println(tmp);
}
System.out.println(a);
}
}
5.2.4 递归方法
方法调用自身,构成递归。
5.2.5 方法重载
Java允许一个类里里定义多个同名方法,只要形参列表不同就行。
如果一个类包含多个同名方法,则称为方法重载。
5.3 成员变量和局部变量
5.3.1 成员变量和局部变量
成员变量值在类里定义的变量,局部变量是在方法里定义的变量。
成员变量分为类变量和实例变量两种,定义成员变量时没有static修饰时就是实例变量,有static修饰就是类变量。
类变量在该类的准备阶段起就存在,直到系统完全销毁这个类,类变量的作用域和类的生存范围相同。
而实例变量则时从实例创建起存在,直到实例被销毁。
@成员变量无需显式初始化。系统会默认初始化。
局部变量根据定义形式不同,可以被分成3种。
形参,方法局部变量,代码块局部变量。
局部变量除了形参之外,都必须显式初始化。
5.3.2 成员变量初始化和内存中的运行机制
系统加载类或创建类的实例时,自动为成员变量分配内存空间,指定初始值。
5.3.3 局部变量的初始化和内存中的运行机制
局部变量定义后,必须经过显式初始化才能使用,系统不会为局部变量执行初始化。
定义局部变量后,系统并未为这个变量分配内存空间,知道赋初始值时,才分配内存。
局部变量不属于类或实例,因此它总是保存在其方法的栈内存中。
5.3.4 变量的使用规则
应该使用成员变量的情形:
1、变量用于描述某个类或对象的属性,比如人的身高,体重,应该使用成员变量。
如果是类相关,比如人的眼睛数量,应该所有人的眼睛都是2个,则应该定义为类变量。
2、变量用来保存类或实例的状态信息。比如五子棋的棋盘数组。
3、变量需要在多个方法间共享。
如果可能,尽量缩小局部变量的范围,节约内存的使用。
5.4 隐藏和封装
5.4.1 理解封装
封装指将对象的状态信息隐藏在对象内部,不允许外界直接访问对象内部信息,而是通过类提供的方法来实现对内部信息的操作和访问。
5.4..2 使用访问控制符
Java提供3个访问控制符:private protected public,代表3个访问控制级别。
另外还有一个默认的级别。
private(当前类访问权限)
default(包访问权限)
protected(子类访问权限)
public(公共访问权限)
@如果一个Java源文件没有publc 类,则Java源文件文件名可以是任意的。如果有public类,则文件名必须和public修饰的类名相同
实例:(使用封装的)Person类
public class Person
{
private String name;
private int age;
public void setName(String name){
if(name.length()>6 || name.length()<2){
System.out.println("人名不符合要求");
return;
}
else{
this.name = name;
}
}
public String getName(){
return this.name;
}
public void setAge(int age){
if(age>100 || age<0){
System.out.println("年龄不合法");
return;
}
else{
this.age = age;
}
}
public int getAge(){
return this.age;
}
}
类外,只能使用setter和getter方法来访问Person的属性。
@关于控制访问符
1、类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的,类似全局变量的成员变量才考虑public。
此外,有些方法只用于辅助其他方法的实现,称为工具方法,也应该用private修饰。
2、如果这个类将作为其他类的父类,里面的方法希望子类重写,而不希望外界调用,应该用protected修饰这些方法。
3、希望暴露给其他类的应该用public。因此,类的构造器应该用public修饰,允许其他地方创建类的实例。
5.4.3 package、import和import static
package用来解决重名问题
Java允许将一组相关的类放在一个package下,构成逻辑上的类库单元。
如果希望将一个类放在指定的包下,应该在源程序的第一个非注释行写下:
package packageName;
则这个文件的类都属于这个包。其他人使用该包下的类时,应该使用包名加类名。
将Hello 放在lee包下:
package lee;
public class Hello
{
public static void main(String[] args){
System.out.println("Hello, world!");
}
}
使用
javac -d . Hello.java 编译,则会出现一个lee文件夹,里面有Hello.class
进入lee文件夹,执行 java lee.Hello
同一个包下的类可以自由访问,无需添加包前缀。
import 语句:
为了避免繁琐的前缀,Java引入了import关键字
import package.subpackage,,,ClassName;
@使用*表示全部类
一旦import导入某些类,就可以省略包前缀。
静态导入:
导入指定类的某个静态成员变量、方法。
语法:
import static package.subpackage…ClassName.fieldName|methodName;
5.4.4 Java的常用包
Java核心类都放在Java包及其子包下,Java许多扩展类都放在javax包及其子包下。
5.5 深入构造器
5.5.1 使用构造器执行初始化
构造器最大的作用就是创建对象时执行初始化。
public class ConstructorTest
{
public String name;
public int count;
public ConstructorTest(String name,int count){
this.name = name;
this.count = count;
}
public static void main(String[] args){
ConstructorTest ct = new ConstructorTest("lee", 22);
}
}
@一旦提供自定义的构造器,系统就不再提供默认的构造器
5.5.2 构造器重载
类似方法重载
import jdk.internal.jshell.tool.resources.l10n;
public class ConstructorOverload
{
public String name;
public int count;
public ConstructorOverload(){}
public ConstructorOverload(String name,int count){
this.name = name;
this.count= count;
}
public static void main(String[] args){
ConstructorOverload co1 = new ConstructorOverload();
ConstructorOverload co2 = new ConstructorOverload("java",111);
}
}
如果包含多个构造器,一个构造器包含另一个构造器全部的执行体。则可以用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(name,color);
this.weight = weight;
}
}
5.6 类的继承
Java的继承是单继承,每个子类只有一个直接父类。
5.6.1 继承的特点
继承通过extends 关键字实现, 被继承的称为父类,实现继承的称为子类。
父类和子类的关系,是一般和特殊的关系,例如水果和苹果的关系。苹果是一种特殊的水果。
继承语法:
修饰符 class SubClass extends Supclass
{
…
}
extends 直译为扩展,可以体现出子类对父类的扩展。
而现在一般翻译为继承,体现子类获得父类的全部成员变量和方法。
@子类不会获得父类的构造器。
例:
public class Fruit
{
public double weight;
public void info(){
System.out.println("我是一个水果,重"+weight+"g!");
}
}
public class Apple extends Fruit
{
public static void main(String[] args){
Apple a = new Apple();
a.weight = 56;
a.info();
}
}
5.6.2 重写父类的方法
大多数时候,子类以父类为基础,扩展新的成员。但有时候,子类要重写父类的方法。例:
public class Bird
{
public void fly(){
System.out.println("起飞...");
}
}
//鸵鸟
public class Ostrich extends Bird
{
public void fly(){
System.out.println("只能在地上跑");
}
}
子类鸵鸟重写(Override)了Bird的fly()方法,重写也称覆盖。
方法重写遵循“两同两小一大”规则:
两同即方法名相同,形参列表相同; 两小指子类方法返回值类型比父类方法返回值类型 更小或相等,子类方法抛出异常比父类方法抛出的异常类更小或相等;
一大值子类方法的访问权限应该比父类方法访问权限更大或相等。
当子类覆盖父类方法后,子类对象无法访问被覆盖的父类方法,但可以在子类方法中调用父类中被覆盖的方法。使用super或父类类名作为调用者来调用被覆盖的方法。
如果父类方法使用private修饰,则对子类隐藏。
5.6.3 super限定
要在子类中方法中调用被覆盖的父类方法,可使用super限定
public class Ostrich extends Bird
{
public void fly(){
System.out.println("只能在地上跑");
}
public void call(){
super.fly(); //调用父类方法
}
}
super用于限定对象调用它从父类继承得到的实例变量或方法。
如果子类定义了和父类同名的实例变量,则会隐藏父类实例变量,通过super可以访问被隐藏的实例变量。
5.6.4 调用父类构造器
子类不会获得父类构造器,子类构造器里可以调用父类构造器的初始化代码,类似于一个构造器调用另一个重载的构造器。
在子类中调用父类父类构造器使用super调用。
class Base
{
public double size;
public String name;
public Base(double size,String name){
this.size = size;
this.name = name;
}
}
public class Sub extends Base
{
public String color;
public Sub(double size,String name, String color){
super(size,name); //调用父类构造器
this.color = color;
}
}
@不管是否使用suoer调用父类构造器,子类构造器总会调用父类构造器一次。
5.7 多态
5.7.1 多态性
编译时类型由声明该变量的类型决定,运行时类型由实际赋给变量的对象决定。
如果运行和编译时类型不一致,就会出现多态(Polymorphism)。
class BaseClass
{
public int book = 6;
public void base(){
System.out.println("父类的普通方法");
}
public void test(){
System.out.println("父类的被覆盖方法");
}
}
public class SubClass extends BaseClass
{
public String book = "java";
public void test(){
System.out.println("子类覆盖父类的方法");
}
public void sub(){
System.out.println("子类的普通方法");
}
public static void main(String[] args){
BaseClass p = new SubClass();
p.base();
p.test(); //
}
}
@与方法不同,对象的实例变量不具有多态性。
5.7.2 引用变量的强制类型转换
如果要一个引用变量调用它运行时类型的方法,则需要强制类型转换成运行时类型。
强制类型转换语法:
(type) variable
强制类型转化并不都可行,因此在强制类型转换之前先通过 instanceof运算符判断是否可以转换:
if(objPri instanceof String){
String str = (String) objPri;
}
5.7.3 instanceof 运算符
前面的操作数为一个引用类型变量, 后一个操作数通常是一个类。
用于判断前面的对象是否是后面的类。
@ 前面的操作数要和后面的类有继承关系,否则会引起编译错误。
5.8 继承和组合
5.8.1 使用继承的注意点
继承带来高度复用的同时,也带来一个严重的问题:继承破坏了父类的封装性,子类可以直接访问父类的内部信息。
为了确保父类良好的封装性,设计父类应该遵循以下规则:
1、尽量隐藏父类的内部数据,尽量将父类成员设置为private。
2、不要让子类可以随意访问,修改父类的方法。父类中的(仅仅辅助其他功能实现的)工具方法,应该使用private修饰。
如果父类中方法必须为外部使用,又不想子类重写,可以使用final修饰符。 如果希望子类重写,又不希望外部类访问,可以使用protected。
3、尽量不要在父类构造器调用将要在子类中重写的方法。
@final class xx final修饰的类不能被继承
5.8.2 利用组合实现复用(has-a)
如,借助Arm类实现Person类。
5.9 初始化块
与构造器作用类似。
5.9.1 使用初始化块
初始化块是Java类中可以出现的第4种成员。一个类可以有多个初始化块,按先后顺序执行。
初始化块的语法:
[修饰符] {
//初始化块的代码
}
@修饰符可以是static,称为静态初始化块
当创建Java对象时,系统总是先调用类定义的初始化块。
5.9.2 初始化块和构造器
某种程度上,初始化块是构造器的补充,如果两个构造器都有共同的无需参数的初始化行为,则可以提取到初始化块中。
@实际上初始化块是一种假象,使用javac编译后,初始化块会被移动到每个构造器中。
5.9.3 静态初始化块
使用static修饰符的初始化块。静态初始化块是和类相关的,总是在类初始化阶段执行。
静态初始化块和声明静态成员变量时指定初始值 都是类的初始化代码,执行顺序和代码排列顺序相同。