第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修饰符的初始化块。静态初始化块是和类相关的,总是在类初始化阶段执行。
静态初始化块和声明静态成员变量时指定初始值 都是类的初始化代码,执行顺序和代码排列顺序相同。