1. 类和对象
1.1 JAVA 类
- Java类可被认为是一种自定义的数据类型, 可以使用类定义变量, 所有使用类定义的变量都是引用变量,它们将会引用到类的对象;
- 面向对象程序设计过程中有两重要概念: 类(class)和对象(object, 也被称为实例, instance), 其中类是某一批对象的抽象, 也可以将类理解为某一种概念; 对象才是一个具体存在的实体.
- 面向对象三大特征: 封装, 继承, 多态
- 类的作用:
- 定义变量
- 创建对象
- 调用类的类方法或访问类的类变量
- 派生子类
定义类的语法
[修饰符] class 类名
{
成员变量 (field)
方法(method)
构造器(constructor)
内部类(nested class)
初始化模块
}
语法说明
- 修饰符: 可以是 public, final | abstract ("|"表示互斥), 或者完全可以省略,
- 类名: 一个合法的标识符(字母,数字,下划线,$, 组成, 但是数字不能开头, 也不要用($)开头),由有意义的单词组成, 每个单词首字母大写, 其他字母小写, 单词之间不要有分隔符
- 一个类最常见的成员: 构造器,成员变量,方法. 这三种都可以是零个或多个,如果零个,表示空类.
- static 修饰的成员不能访问没有 static 修饰的成员
- 花括号"{", "}" 之间的部分叫做" 类体 "
成员变量(field)
用于定义该类或该类的实例所包含的状态数据; 分为类变量和实例变量
定义语法
[修饰符] 类型 成员变量名 [=默认值;]
语法说明
- 修饰符: private | protected | public, final, static, (transient: 序列化相关)
- 类型: 任意基本类型或者引用类型
- 变量名: 驼峰命名法, 首字母小写 后面每个单词首字母大写, 尽量使用与项目相关的描述性的名词
- static修饰成员变量表明它属于这个类本身, 而不属于该类的单个实例, 所以static修饰的变量叫做类变量, 通常把不是用static修饰的成员变量或方法也叫做实例变量, 实例方法.
方法(method)
用于定义该类或该类的实例的行为特征或者功能实现
定义语法
[修饰符] 方法返回值类型 方法名(形参列表)
{
// 代码: 由零条或者多条可执行语句组成的方法体
// 代码: 定义变量(包括数组), 变量赋值, 流程控制, 数据语句
// 如果声明了返回值类型, 必须有return语句
}
语法说明
- 修饰符: private | protected | public, final | abstract, static,
- 方法返回值类型: 可以是任意基本类型或引用类型, 可使用 void 声明没有返回值
- 方法名: 驼峰写法, 通常使用与项目相关的动词
- 形参列表: 定义该方法可以接受的参数, 可由零组或多组"参数类型 形参名" 组合而成, 多组参数之间用(,) 隔开, 例如: 形参类型1 形参名1, 形参类型2 形参名2, ...
- 方法体中多条可执行语句之间有严格的执行顺序, 排在方法体前面的语句总是先执行, 排在
方法体后面的语句总是后执行 - 花括号"{", "}" 之间的部分叫做" 方法体 "
构造器
构造器是一个类创建对象的根本途径, 如果一个类没有构造器, 这个类通常无法创建实例, 使用 new 调用构造器创建对象。(所有的类都有构造器,即使不定义构造器,系统也会默认的创建无形参的构造器
)
定义语法
[修饰符] 构造器名(形参列表)
{
// 代码: 定义变量(包括数组), 变量赋值, 流程控制, 数据语句
}
语法说明
- 修饰符: private | protected | public 其中之一
- 构造器名: 构造器名必须和类名相同
- 形参列表: 和定义方法形参列表的格式完全相同
- 注意: 构造器既不能定义返回值类型, 也不能使用void声明构造器没有返回值
- 定义类的代码没有定义构造器, 系统将为它提供一个默认的构造器, 系统提供的构造器总是没有参数的
// 例子: 定义一个Person类
public class Person
{
// 定义两个成员变量(实例变量)
public String name;
public int age;
// 定义一个say方法(实例方法)
public void say(String content)
{
System.out.println(content);
}
// 定义一个构造器(无参数构造器)
public Person()
{
System.out.println("创建的第一个构造器");
}
}
1.2 对象的产生和作用
创建对象的根本途径是构造器, 通过 new 关键字来调用某个类的构造器即可创建这个类的实例
Java对象的作用
- 访问对象的实例变量
- 调用对象的方法
类或实例访问方法或成员变量的语法
- 类.类变量|方法
- 实例.实例变量|方法
- static 修饰的方法或成员变量, 既可通过类来调用, 也可通过实例来; 没有 static 修饰的普通方法和成员变量, 只能通过实例来调用
// 例子:
// 使用上面定义的 Person 类来创建一个 Person 类型的变量(因为类也是一种自定义的数据类型)
Person p;
// 通过 new 关键字调用 Person 类的构造器, 返回一个 Person 实例
// 将该 Person 实例赋给P变量
p = new Person();
// 上面你的代码也可以简化; 定义P变量的同时并为p变量赋值
// Person p = new Person();
// 访问p的name和age实例变量, 直接为该变量赋值
p.name = "jefxff";
p.age = 19;
// 调用p的say()方法, 声明 say() 方法的时候定义一个形参
// 调用该方法必须为形参指定一个定义时声明类型的值
p.Say("Java is so hard! But I can do it!")
// 直接输出 p 的 name 实例变量
System.out.println(p.name);
1.3 对象, 引用和指针
- 结合上面你的Java代码的例子, 代码: Person p = new Person(); 这行代码创建了一个 Person实例, 也被称之为Person对象; 这行代码实际产生了两个东西:一个是p变量, 一个是Person对象. 这个Person对象被赋给p变量
- 类也是一种引用类型, 因此程序中定义的Person类型的变量(p)实际上是一个引用, 他被存放在栈内存中, 指向实际的Person对象; 而真正的Person对象则存放在堆(heap)内存中
- 不管是数组还是对象,当程序访问引用变量的成员变量或方法时, 实际上是访问该引用变量所引用的数组, 对象的成员变量或方法.
- 栈内存里面的引用变量并未真正存储对象的成员变量, 对象的成员变量数据实际上存放在堆内存里,而引用变量只是指向该堆内存里面的对象
- 所以, 引用变量与 C 语言里的指针很像, 他们都是存储一个地址值, 通过这个地址来引用到实际对象, 只是 Java 对这个指针进行了封装
- 堆内存里的对象可以有多个引用, 即多个引用变量指向同一个对象, 代码: Person p2 = p; 这行代码把p变量的值赋值给 p2 变量, 也就是将 p 变量保存的地址值赋给 p2 变量, 这样 p2 变量和 p 变量将指向堆内存里的同一个 Person 对象, 所以不管访问 p 变量还是访问 p2 变量的成员变量和方法, 他们实际上是访问同一个 Person 对象的成员变量和方法, 将会返回相同的结果
1.4 对象的 this 引用
- this 关键字总是指向调用该方法的对象. 根据 this 出现位置, 有如下两种情形:
出现在构造器中, this 就代表该构造器正在初始化的对象;
出现在非 static 方法中(实例方法), this就代表了该方法的拥有者; - this关键字最大的作用就是让类中一个方法, 访问该类里的另一个方法或实例变量
- 谁调用这个方法, this就代表谁
- this. 的很重要的作用是: 用于区分方法或构造器的局部变量. 尤其是与成员变量同名时--更需要this进行区分
- 如果在 static 修饰的方法(静态方法)中使用this关键字, 则这个关键字就无法指向合适的对象, 所以static修饰的方法不能使用this引用. 因此 static 修饰的方法不能访问不使用 static 修饰的普通成员, 即: 静态成员不能直接访问非静态成员.
// 例子: 1. 没有使用this的情况下, 下面例子调用内部方法需要创建两个实例来实现
/**
* 下面的程序中一共产生了两个 Dog 对象, 调用run() 方法的是 dog , 而在 run() 方法中调用jump() 方法的是 d
* 再记一遍, **没有 static 修饰的成员变量和方法都必须使用对象来调用**
*/
public class Dog
{
// 定义一个jump 方法
public void jump()
{
System.out.println("正在执行 jump 方法.....");
}
// 定义一个 run() 方法, run()方法需要借助 jump() 方法
public void run()
{
Dog d = new Dog();
d.jump();
System.out.println("正在执行 run 方法.....");
}
public static void main(String[] args)
{
// 创建 dog 对象
Dog dog = new Dog();
// 调用 Dog 对象的 run() 方法
dog.run();
}
}
// 例子: 2. 使用this方法, 代码同上面例子1, 但是只需要创建一个实例
public class Dog
{
// 定义一个jump 方法
public void jump()
{
System.out.println("正在执行 jump 方法.....");
}
// 定义一个 run() 方法, run()方法需要借助 jump() 方法
public void run()
{
// 谁调用这个方法, this就代表谁
this.jump();
// 这是一种省略 this 的写法, 但是我属于刚开始学, 就不要省略了
// jump();
System.out.println("正在执行 run 方法.....");
}
public static void main(String[] args)
{
// 创建 dog 对象
Dog dog = new Dog();
// 调用 Dog 对象的 run() 方法
dog.run();
}
}
// 例子: 3. 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
System.out.println(new ThisInConstructor().foo); // out: 6
// 创建对象, 使用对象输出 foo
ThisInConstructor t = new ThisInConstructor();
System.out.println(t.foo); // out: 6
}
}
2. 方法详解
2.1 方法的所属性
- 方法是类或者对象的行为特征的抽象, 方法是类或对象的最重要组成部分;
- Java里面的方法不能独立存在, 所有的方法都必须定义在类里面; 方法在逻辑上要么属于类, 要么属于该类的一个对象;
- Java中, 类是一等公民, 整个系统由一个一个的类组成, 一旦将一个方法定义在某个类体里面, 如果这个方法使用了static修饰, 则这个方法属于这个类, 否则这个方法属于这个类的实例;
- Java中执行方法时必须使用类或者对象来作为调用者, 即所有的方法都必须使用 "类.方法" 或者使用类创建出来的对象通过 "对象.方法" 的形式来调用;
- 同一个类的一个方法调用另外一个方法: 如果调用的是普通方法, 则默认使用 this 作为调用者, 如果被调用的方法是静态方法(类方法), 则默认使用 类 作为调用者.
- 记住: 注意: 使用 static 修饰的方法既可以使用类作为调用者来调用, 也可以使用对象作为调用者来调用; 非 static 方法则属于该类的对象, 不属于类本身, 所以 非static方法只能使用对象来调用.
// 例子:
public class Dog
{
// 定义3个成员变量(实例变量)
public String name = "旺财";
public String color;
public double weight;
// 定义一个方法 (实例方法)
public void jump()
{
System.out.println(this.name + " 正在执行 jump 方法.....");
}
// 定义一个run方法(实例方法)
public void run()
{
// Dog d = new Dog();
// d.jump();
this.jump();
System.out.println("正在执行 run 方法.....");
}
// 定义一个 static 修饰的 barking 方法 (类方法)
public static void barking()
{
System.out.println("wangwangwang............");
}
// 程序的入口 main方法
public static void main(String... args)
{
Dog dog = new Dog();
// 实例调用非static修饰的方法
dog.run();
// 测试了, 现在再记住一次, 没有static修饰的方法, 只能用实例来调用, 下面用类 Dog
// 调用没有static修饰的方法, 报错误:无法从静态上下文中引用非静态 方法 run()
// Dog.run();
System.out.println();
// 实例调用static修饰的方法
dog.barking();
// 类调用static修饰的方法
Dog.barking();
}
}
2.2 方法的参数传递机制
参数传递方式:
- Java里方法的参数传递方式只有一种: 值传递. 值传递就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响;
- 值传递的实质: 当系统开始执行方法时,系统为形参执行初始化, 就是把实参变量的值赋给方法的形参变量, 方法里操作的并不是实际的实参变量.
// 例子1: 基本类型的参数传递效果
public class PrimitiveTransferTest
{
// 定义一个类方法,接受两个int类型的参数
public static void swap(int a, int b)
{
// 下面三行代码实现a, b 变量的值交换(python中: a, b = b, a 一句话就转换了)
// 定义一个临时变量来保存 a 变量的值
int tmp = a;
// 把 b 的值赋给 a
a = b;
// 把临时变量 tmp 的值赋给 b
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); // out: a = 9, b = 6
// 下面代码输出: a = 6, b =9
System.out.println("交换结束后: a的值是: " + a + "; 变量b的值是: " + b );
System.out.println();
}
}
// 程序说明:
// 程序的运行结果说明: swap()方法里 a和b 的值是 9和6, 交换结束后, 变量a和变量b的值依
// 然是 6和9. 说明mian方法里面的变量a和b, 并不是swap()方法里的a和b, swap()方法的a和b只
// 是main()方法里变量a和b的复制品.
// 例子2:引用类型的参数传递效果
class DataWrap
{
int a;
int b;
}
public class ReferenceTransferTest
{
public static void swap(DataWrap dw)
{
// 下面三行代码实现 dw 的 a, b两个成员变量的值交换
// 定义一个临时变量来保存dw对象的a成员变量的值
int tmp = dw.a;
//把dw对象的b成员变量的值赋给a成员变量
dw.a = dw.b;
//把临时变量tmp的值赋给dw对象的b成员变量
dw.b = tmp;
System.out.println("swap方法里, a成员变量的值是: " + dw.a +
"; b成员变量的值是: " + dw.b );
}
public static void main(String[] args)
{
DataWrap dw = new DataWrap();
dw.a = 6;
dw.b = 9;
swap(dw);
System.out.println("交换结束后, a成员变量的值是: " + dw.a +
"; b成员变量的值是: " + dw.b );
}
}
// 程序说明:
// 1.引用类型的参数传递还是值传递的方式, 系统一样复制了 dw 的副本传入 swap() 方法, 但
// 关键在于dw只是一个引用变量, 所以系统复制了 dw 的变量, 但并未复制DataWrap对象.
// 2. 由于dw只是一个引用变量, 故实际操作的还是堆内存中的DataWrap对象.
// 3. 在 swap方法中 将 dw 赋值为null 后, 不会影响main方法中dw变量操作.
2.3 形参个数可变的方法
传递类型
- (类型... 形参名) 这种传递形参是个数可变的, 本质就是数组; 这种写法的好处是: 调用方法更加方便, 既可直接传入多个元素, 系统会自动将他们封装成数组, 也可以用数组; 缺点是: 这种写法只能作为形参列表的最后一个形参;(
暗示: 一个方法最多只能有一个"个数可变"的形参
) 这种写法等同于下面的2类型:
如定义形参: public static void test(int a, String... books);
传递形参:test(6, "python", "java", "C", "JavaScript"); - (类型[] 形参名) 这种写法的好处是: 这种数组形式的形参可以出现在形参列表的任意位置.
如定义形参: public static void test(int a, String[] books);
传递形参: test(6, new String[]{"python", "java", "C", "JavaScript"});
// 例子1:形参个数可变的方法
public class Varargs
{
//定义一个形参个数可变的方法
public static void test(int a, String... books)
{
//books 被当成数组处理, 使用foreach循环输出 (顺便回忆一下循环结构)
for (String book : books)
{
System.out.println(book);
}
//输出a变量的值
System.out.println(a);
}
public static void main(String[] args)
{
//直接用类调用static修饰的方法(应该是 Varargs.test()但是省略了...)
test(6, "python", "java", "C", "JavaScript");
// 调用test的时候, 形参传入一个数组
test(6, new String[]{"python1", "java1", "C1", "JavaScript1"});
}
}
2.4 递归方法
- 递归就是一个方法体内部调用它自身. 递归方法包含了一种隐式的循环, 他会重复执行某段代码,但这种重复执行无需循环控制, 但是还是要有让递归停止的条件, 并且这种条件存在, 否则就死循环了.
- 递归必须一定要向已知的方向递归.
// 例子:
// 计算: 已知一个数列, f(0)=1, f(1)=4, f(n+2) = 2*f(n+1) + f(n); 求: f(10)的值
public class recursive
{
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);
}
}
public static void main(String[] args)
{
System.out.println(fn(10));
}
}
// 程序说明:
// 如上程序: 当一个方法不断地调用它本身的时候, 必须在某个时刻方法的返回值是确定的, 即不
// 再调用它本身, 否则这种递归就变成了无穷递归, 类似与死循环,
2.5 方法重载
- Java允许同一个类里面定义多个同名方法,只要形参列表不同就行. 如果同一个类里面包含了多个方法名相同, 但形参列表不同的方法,则被称之为方法重载
- 调用者, 也就是方法的拥有者, 既可以是类, 也可以是对象
- 方法名, 方法的标识必须相同
- 形参列表, 当调用方法时, 系统将会根据传入的实参列表匹配
- 两同一不同: 同一个类中方法名相同, 参数列表不同; 其他的方法返回值类型, 修饰符等与方法重载无关.
- 尽量不要定义长度可变的形参列表
// 例子: 方法的重载
public class OverloadVarargs
{
public void test(String msg)
{
System.out.println("只有一个字符串参数的test方法" + msg);
}
public void test(String name, String gender)
{
System.out.println("形参长度为2的test方法"
+ name + gender);
}
// 因为前面已经定义了两个test()方法, test()方法里面有一个字符串参数和两个字符串
// 此处的长度可变参数里不包含一个字符串参数的形式
public static void test(String... books)
{
System.out.println("#-----形参长度可变的test方法-----#");
}
public static void main(String[] args)
{
OverloadVarargs orl = new OverloadVarargs();
orl.test("注意看 ");
orl.test("水岛津实", "女");
orl.test(new String[]{"python1", "java1", "C1", "JavaScript1"});
}
}
3. 成员变量和局部变量
3.1 成员变量
- 成员变量指在类里面定义的变量,也就是前面学过的field(用于定义该类或该类的实例所包含的状态数据; 包含类变量和实例变量)
- 类变量:从该类的准备阶段起开始存在, 直到系统完全销毁这个类, 类变量的作用域与这个类的生存范围相同
- 实例变量: 从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例变量的作用域与对应实例的生存范围相同
- 命名规则: 多个有意义的单词连缀而成, 第一个单词首字母小写, 后面单词首字母大写
- 如果通过一个实例修改了类变量的值, 由于这个类变量不属于它, 而是属于它所对应的类,因此修改的依然是该类的类变量, 与通过该类来修改类变量的结果完全相同, 这会导致该类的其他实例来访问这个类变量时也将获得这个被修改过的值
成员变量访问语法
- 类变量, 也就是类成员变量, 可通过类或者实例访问
类.类变量
实例.类变量 - 实例变量, 也就是实例成员变量
实例.实例变量
// 例子:
class Person
{
// 定义一个实例变量
public String name;
//定义一个类变量
public static int eyeNum;
}
public class PersonTest
{
public static void main(String[] args)
{
// 第一次主动使用Person类,该类自动初始化, 则eyeNum变量开始作用,输出0
System.out.println("Person类的eyeNum类变量值:" + Person.eyeNum);
// 创建Person对象
Person p = new Person();
// 通过Person对象的引用p访问Person对象name实例变量
// 并通过实例访问eyeNum类变量
System.out.println("p变量的name变量的值是: " + p.name
+ " p对象的eyeNum变量值是: " + p.eyeNum);
// 直接为 name 实例变量赋值
p.name = "孙悟空";
//通过p访问的 eyeNum 类变量, 依然是访问Person的eyeNum类变量
p.eyeNum = 2;
// 再次通过Person对象来访问name实例变量和eyeNum类变量
System.out.println("p变量的name变量的值是: " + p.name
+ " p对象的eyeNum变量值是: " + p.eyeNum);
// 前面通过p修改了Person的eyeNum,此处的Person.eyeNum将输出2
System.out.println("Person类的eyeNum类变量值:" + Person.eyeNum);
// 再创建一个实例对象
Person p2 = new Person();
// p2访问的eyeNum类变量依然引用Person类的, 因此输出2
System.out.println("p2对象的eyeNum类变量值: " + p2.eyeNum);
}
}
// 程序说明:
// 1. 成员变量无需显示的初始化, 只要为一个类定义了类变量或者实例变量, 系统就会在这
// 个类的准备阶段或创建该类的实例时进行默认的赋值, 成员变量默认初始化时的赋值规
// 则与数组动态初始化时数组元素的赋值规则完全相同;
// 2. 类变量的作用域大于实例变量的作用域: 实例变量随实例的存在而存在, 而类变量则随
// 类的存在而存在. 实例也可以访问类变量, 同一个类的所有实例访问类变量时, 实际上
// 访问的是该类本身的同一个变量, 也就是说, 访问了同一片内存.
3.2 局部变量
- 局部变量就是指在方法里面定义的变量
- 局部变量除了形参之外(形参的初始化在调用该方法时由系统完成, 值由方法的调用者指定),都必须显式的初始化; 也就是说, 必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们.
- 方法的局部变量, 其作用域从定义该变量开始, 直到方法结束
局部变量包含
- 形参 (方法签名中定义的变量): 作用域在整个方法体内有效
- 方法局部变量 (在方法中定义): 作用域是从定义该变量的地方生效, 该方法结束时失效
- 代码块局部变量 (在代码块内定义): 作用域从定义该变量的地方生效, 到该代码块结束时失效
// 例子1:代码块局部变量例子
public class BlockTest
{
public static void main(String[] args)
{
// 代码块
{
// 定义一个代码块局部变量a
int a;
// 下面代码将出现错误, 因为a变量还未初始化
// BlockTest.java:10: 错误: 可能尚未初始化变量a
// System.out.println("代码块局部变量a的值: " + a);
// 为a 变量赋初始值, 也就是进行初始化
a = 5;
System.out.println("代码块局部变量a的值: " + a);
}
// 在代码块的外面访问a变量, 并不存在
// System.out.println( a);
}
}
//例子2: 方法体内局部变量例子
public class BlockTest
{
public static void main(String[] args)
{
// 定义一个方法局部变量
int a ;
// 下面的代码是错误的, 因为a变量未初始化
// System.out.println("方法局部变量a的值: " + a);
// 为a 变量赋初始值, 也就是进行初始化
a = 5;
System.out.println("方法局部变量a的值: " + a);
}
}
成员变量局部变量NOTES:
- 同一个类里面, 成员变量的作用范围是整个类里面有效
- 一个类里不能定义两个同名的成员变量, 即使一个是类变量, 一个是实例变量都不行
- 一个方法里不能定义两个同名的方法局部变量, 方法局部变量与形参也不能同名
- 同一个方法中不同代码块内的代码块局部变量可以同名;
- 如果先定义代码块局部变量, 后定义方法局部变量, 前面定义的代码块局部变量与后面定义的方法局部变量也可以同名
- Java 允许局部变量和成员变量同名, 如果方法里的局部变量和成员变量同名, 局部变量会覆盖成员变量, 如果需要在这个方法里引用被覆盖的成员变量, 则可以使用this(对于实例变量) 或类名(对于类变量)作为调用者来限定访问成员变量.
// 例子3: 局部变量覆盖成员变量
public class VariableOverrideTest
{
// 定义一个 name 实例变量
public String name = "李刚";
// 定义一个price类变量
public static double price = 78.0;
// 主方法程序入口
public static void main(String[] args)
{
// 方法里的局部变量, 局部变量覆盖成员变量
int price = 65;
// 直接访问 price 变量, 将输出 price 局部变量的值:65
System.out.println(price);
// 使用类名作为price变量的限定
// 将输出price类变量的值: 78.0
System.out.println(VariableOverrideTest.price);
// 运行info 方法
new VariableOverrideTest().info();
}
public void info()
{
// 方法里的局部变量, 局部变量覆盖成员变量
String name = "孙悟空";
// 直接访问name变量, 将输出name局部变量的值: "孙悟空"
System.out.println(name);
// 使用this来作为name变量的限定
// 将输出name实例变量的值: "李刚"
System.out.println(this.name);
}
}
3.2 成员变量的初始化和内存中的运行机制
- 当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。
- (首次加载类时)在类的准备阶段,系统将为该类的类变量分配内存空间,并指定默认初始值
- 实例变量是在创建实例时分配内存空间并指定初始值
- 通过实例.类变量来访问类变量时,由于实例变量并没有保存类变量,所以访问的还是该类的类变量;所以所有的实例访问类变量都是访问的类的类变量,即同一块内存区
- 所以由于上面4的原因,访问类变量时,尽量使用类作为主调
3.3 局部变量的初始化和内存中的运行机制
- 定义局部变量后,系统并未为这个变量分配内存地址,直到等到程序为这个变量赋初始值的时候系统才会为局部变量分配内存,并将初始值保存到这块内存中
- 局部变量不属于任何类或者实例,因此局部变量总是保存在其所在方法的栈内存中
- 若局部变量是基本类型的变量:则直接把这个变量的值保存在该变量对应的内存中
- 若局部变量是引用类型的变量:则这个变量里边存放的是地址,通过地址引用到该变量实际引用的对象或数组
- 局部变量所占的内存区通常比较小
- 栈内存中的变量无须系统回收,往往随方法或代码块的运行结束而结束
3.4 变量的使用规则
- 能用局部变量就不用成员变量
- 使用局部变量也应该尽可能缩小局部变量的使用范围
使用成员变量的场景
- 若需要定义的变量是用于描述某个类或某个对象的固有信息
如果:这种信息对这个类的所有实例完全相同,这种变量应定义为类变量
如果:这种信息是实例相关的,这种变量应定义为实例变量 - 如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息,这种用于保存某个类或某个实例状态信息的变量通常应该使用成员变量
- 如果某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存
扩大变量的作用域的缺点
- 增大了变量的生存时间,导致更大的内存开销
- 扩大了变量的作用域,不利于提高程序的内聚性
// 例子:
public class ScopeTest1{
//定义一个类成员变量作为循环变量
static int i;
public static void main(String[] args){
for (i = 0; i < 10; i++){
System.out.println("hello!");
}
}
}
public class ScopeTest2
{
public static void main(String[] args)
{
//定义一个方法局部变量作为循环条件
int i;
for (i = 0; i < 10; i++)
{
System.out.println("hello!");
}
}
}
public class ScopeTest3
{
public static void main(String[] args)
{
// 定义一个代码块局部变量作为循环变量
for(int i = 0; i < 10; i++)
{
System.out.println("hello!");
}
}
}
// 说明: 上面三个程序的执行结果完全一样, 但是程序的效果则大有差异. 第三个程序最符合软件的
// 开发规范;
4. 隐藏和封装
4.1 理解封装
封装(Encapsulation) 是面向对象的三大特征之一(还有继承和多态), 它指的是将对象的状态信息(如成员变量)隐藏在对象内部, 不允许外部程序直接访问对象内部信息, 而是通过该类所提供的方法来实现对内部信息的操作和访问
封装的目的
- 隐藏类的实现细节
- 让使用者只能通过事先预定的方法来访问数据, 从而可以在该方法里加入控制逻辑, 限制对成员变量的不合理访问
- 可进行数据检查, 从而有利于保证对象信息的完整性
- 便于修改, 提高代码的可维护性
封装的规则
- 将对象的成员变量和实现细节隐藏起来, 不允许外部访问
- 把方法曝露出来, 方法来控制对这些成员变量进行安全访问和操作
4.2 使用访问控制符
- 控制级别:从左至右, 控制级别从小到大
private ----------> default ----------> protected ----------> public - 访问控制符用于控制一个类的成员是否可以被其他类访问, 对于局部变量而言, 其作用域就是它所在的方法, 不可能被其他类访问, 因此不能使用访问控制符来修饰.
- 外部类只能有两种访问控制级别: public 和默认default(可省略不写), 外部类不能使用private和 protected 修饰. 原因是: 外部类没有处于任何类的内部, 也就没有其所在类的内部, 所在类的子类两个范围, 因此 private 和 protected 访问控制符对外部类没有意义
- 外部类可以使用public 和 包 访问控制权限, 使用 public 修饰的外部类可以被所有类使用,
- 不使用任何访问控制符修饰的外部类只能被同一个包中的其他类使用
控制级别
- private (当前类访问权限):
如果类里面的一个成员(包括成员变量, 方法和构造器等)使用 private 访问控制符来修饰, 则 这个成员只能在当前类的内部被访问. 适用于修饰成员变量, private修饰成员变量可以把成员变量隐藏在该类的内部 - default (包访问权限):
如果类里面的一个成员(包括成员变量, 方法和构造器等)或者一个外部类不使用任何访问控制符修饰, 就称它是包访问权限, default 访问控制的成员或者外部类可以被相同包下的其他类访问 - protected (子类访问权限):
如果一个成员(包括成员变量, 方法和构造器等)使用 protected 访问控制符修饰, 那么这个成员既可以被同一个包中的其他类访问, 也可以被不同包中的子类访问. 在通常情况下, 如果使用protected 来修饰一个方法, 通常是希望其子类来重写这个方法 - public (公共访问权限):
这是一个最宽松的访问控制级别, 如果一个成员(包括成员变量, 方法和构造器等) 或者一个外部类使用 public 访问控制符修饰, 那么这个成员或外部类就可以被所有类访问, 不管访问类和被访问类是否处于同一个包中, 是否具有父子继承关系
访问控制符的使用原则
- 类里面的绝大部分成员变量都应该使用 private 修饰, 只有一些 static 修饰的, 类似全局变量的成员变量, 才考虑使用 public 修饰, 另外: 有些方法只用于辅助实现该类的其他方法, 这些方法被称之为工具方法, 工具方法也应该使用 private 修饰
- 如果某个类主要用做其他类的父类, 该类里包含的大部分方法可能仅希望被其子类重写, 而不想被外界直接调用, 应该用 protected 修饰这些方法
- 希望暴露出来给其他类来调用的方法应该使用 public 修饰, 例如构造器
// 例子:
// 一个实现了合理封装的 Person 类
public class Person
{
// 使用private修饰成员变量, 将这些成员变量隐藏起来
private String name;
private int age;
//提供方法来操作 name 成员变量
public void setName(String name)
{
// 执行合理性校验, 要求用户名必须在2~6位之间
if (name.length() > 6 || name.length() < 2)
{
System.out.println("设置的人名不符合要求!");
return;
}
else
{
this.name = name;
}
}
public String getName()
{
return this.name;
}
// 提供方法来操作age成员变量
public void setAge(int age)
{
// 执行合理型校验, 要求输入的年龄必须在 0~100 之间
if (age > 100 || age < 0)
{
System.out.println("输入的年龄不合法!");
return;
}
else
{
this.age = age;
}
}
public int getAge()
{
return this.age;
}
}
/** 说明:
* 1. setter 和 getter 方法
* 类里面定义了一个 abc 的实例变量, 则其对应的 setter 和 getter 方法名应该为
* setAbc() 和 getAbc() (即将原来实例变量名首字母大写, 并在前面分别增加 set
* 和 get 动词)
* 2. 如果一个Java类的每个实例变量都被使用 private 修饰, 并为每个实例变量都提供了
* public修饰的 setter 和 getter 方法, 那么这个类就是符合JavaBean规范的类
* 3. 进行程序设计的时, 应尽量避免一个模块直接操作和访问另外一个模块的数据, 模块
* 的设计追求高内聚(尽可能把模块的内部数据, 功能实现细节隐藏在模块内部独立完
* 成,不允许外部直接干预), 低耦合(仅暴露少量的方法给外部使用)
*/
// 测试上面的Person类
public class PersonTest
{
public static void main(String[] args)
{
Person p = new Person();
// 因为age成员变量以被隐藏, 所以下面的语句出现编译错误
// p.age = 1000; // out: 错误: age 在 Person 中是 private 访问控制
// 下面语句编译不会出现错误, 但是运行时提示"输入的年龄不合法"
// 程序不是修改 p 的 age 成员变量
p.setAge(1000);
// 访问 p 的 age 成员变量也必须通过其对应的 getter 方法
// 因为上面从未成功设置 p 的 age 成员变量, 故此处输出0
System.out.println("未设置 age 的成员变量时: " + p.getAge());
// 成功修改 p 的 age 成员变量
p.setAge(30);
// 因为上面成功设置了 p 的 age 成员变量, 故此处输出30
System.out.println("成功设置 age 的成员变量后: " + p.getAge());
// 不能直接操作 p 的 name 成员变量, 只能通过其对应的 setter 方法
// 因为 "李刚" 字符串的长度满足2~6, 所以可以成功设置
p.setName("李刚");
System.out.println("成功设置 name 的成员变量后: " + p.getName());
}
}
4.3 package, import 和 import static
包(package)机制
- 包机制提供了类的多层命名空间, 用于解决类的命名冲突, 类文件管理等问题.
- Java允许将一组功能相关的类放在同一个 package 下, 从而组成逻辑上的类库单元
- 位于包中的类, 在文件系统中也必须有与包名层次相同的目录结构
- Java源文件中使用了package语句, 那么源文件里定义的所有类都属于这个包, 位于包中的每个类的完整类名都应该是 包名和类名 的组合, 如果其他人需要使用该包下的类, 也应该使用包名加类名的组合
- 一个Java源文件只能指定一个包, package 语句必须是第一条非注释型语句
- 同一个包下的类可以自由访问, 无须添加包前缀
包的引入语法
package packageName;
// 例子1: 简单的引入包例子
package lee;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Hello, world!");
}
}
/**
*说明:
* 1. 执行 " javac -d . hello.java " 命令, 会在当前的目录下生成一个lee文件夹, 编
* 译后的 hello.class 文件就在 lee 的这个目录中, 而且 也必须在这个 lee 目录下
* 才有效
* 2. 在 lee 文件夹所在目录执行: " java lee.hello " 命令执行程序
* 3. 同一个包中的类不必位于相同的目录下, 只要将路径添加到 CLASSPATH 环境变量里就行
* 4. java 的源文件应放在与包名一致的目录结构下
* 5. 在同一个包的子包中的类, (就是不再当前目录时), 引入类需要添加类前缀: 父包.子包.类
*/
// 例子2:
// 在lee包下面再定义一个sub子包, 定义一个Apple类,
package lee.sub;
public class Apple{
public static void main(String[] args)
{
System.out.print("Hello Apple!");
}
}
// 例子3:
// 定义HelloTest 测试Hello 类
package lee;
public class HelloTest{
public static void main(String[] args){
// 直接访问相同包下面的另一个类, 无需使用包前缀(lee.Hello())
Hello h = new Hello();
// 调用包中包(嵌套包)下面的类时, 就需要添加包前缀
lee.sub.Apple a = new lee.sub.Apple();
}
}
import 关键字
- import可以向某个Java文件导入指定包层次下某个类或全部类
- import 语句应该放在 package 语句之后, 类定义之前
- 作用: 使用 import 可以省略写包名
import 用法
- 导入单个类
import package.subpackage...className;
如: import lee.sub.Apple; - 导入包下的全部类
import package.subpackage...;*
// 例子:
// 改写上面的例子3; 使用import 语句可以简化编程
package lee;
// 使用 import 导入 lee.sub.Apple 类
import lee.sub.Apple;
public class Hellotest
{
pubic static void main(String[] args)
{
// 使用类全面写法
lee.sub.Apple a = new lee.sub.Apple();
// 如果使用 import 语句来导入 Apple 类, 就可以不再使用类全名了
Apple a = new Apple();
}
}
import static 静态导入
- 用于导入指定类的某个静态成员变量, 方法或全部静态成员变量及方法
- import static 语句应该放在 package 语句之后, 类定义之前, 和 import 语句相同位置
- 静态成员变量就是前面学过的类变量, 类方法, 他们都使用 static 修饰
- 使用 import static 则可以连类名都省略
导入语法
- 导入指定类的单个成员变量, 方法
import static package.subpackage...ClassName.fieldName|methodname
例如: import static java.lang.System.out; - 导入指定类的全部成员变量, 方法
import static package.subpackage...ClassName.;*
// 例子:
import static java.lang.System.*;
import static java.lang.Math.*;
public class StaticImportTest
{
public static void main(String[] args)
{
// out 是 java.lang.System 类的静态成员变量, 代表标准输出
// PI 是 java.lang.Math 类的静态成员变量, 代表π常量
out.println(PI);
// 直接调用 Math 类的 aqrt 静态方法
out.println(sqrt(356));
}
}
/**
* Java 源文件结构:
* // 0 个或 1个, 必须放在文件开始
* package 语句
* // 0 个或多个, 必须放在类定义之前
* import | import static 语句
* // 0 个或1个public类, 借口或枚举定义
* public ClassDefinition | interfaceDefinition | enumDefinotion
* // 0 个或多个普通类, 借口或枚举定义
* classDefinition | interfaceDefinition | enumDefinotion
*/
4.4 Java 的常用包
包的位置
- Java的核心类都放在Java包以及其子包下, Java扩展的许多类放在 Javax 包以及其子包下, 这些使用类也就是API(应用程序接口)
常用包
-
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
包含了抽象窗口工具集(Abstract Window toolkits) 的相关类/接口, 这个类主要用于构建图形
用户界面(GUI)程序 -
java.swing
这个包含了Swing 图形用户界面编程的相关类/接口. 这些类可用于构建平台无关的GUI程序
5. 深入理解构造器
5.1 使用构造器执行初始化
- 构造器让系统创建对象时就为该对象的实例变量显式的指定初始值
- 通过 new 关键字调用构造器时, 构造器也确实返回了该类的对象, 但是这个对象并不是完全由构造器负责创建的; 调用构造器时, 系统会先为该对象分配内存空间, 并为这个对象执行默认初始化(动态数组默认赋值一样), 这个对象已经产生了---这些操作在构造器执行之前就已经完成了;
- 当系统开始执行构造器的执行体之前, 系统已经创建了一个对象, 只是这个对象还不能被外部访问, 只能在该构造器中通过 this 来引用. 当构造器的执行体执行结束后, 这个对象作为构造器的返回值被返回, 通常还会赋给另一个引用类型的变量, 从而让外部程序可以访问该对象
- 一旦程序员自定义了构造器, 系统就不再提供默认的构造器
- 通常构造器使用 public 修饰, 允许系统中任何位置的类来创建该类的对象; protected 修饰主要用于被其子类调用; private 修饰用于阻止其他类创建该类的实例
// 例子:
public class ConstructorTest
{
public String name;
public int count;
//提供自定义的构造器, 包含两个参数
Public ConstructorTest(String name, int count)
{
// 构造器里的 this 代表它进行初始化的对象
// 下面两行代码将传入的2个参数赋给 this 代表对象的 name 和 count 实例变量
this.name = name;
this.count = count;
}
public static void main(String[] args)
{
// 使用系统自定义的构造器来创建对象
// 系统将会对该对象执行自定义的初始化
ConstructorTest tc = new ConstructorTest("jefxff", 29);
// 输出 ConstructorTest 对象的 name 和 count 两个实例变量
System.out.println(tc.name);
System.out.println(tc.count);
}
}
5.2 构造器的重载
- 同一个类里具有多个构造器, 多个构造器的形参列表不同, 即被称之为构造器的重载.
- 构造器重载允许Java类里包含多个初始化逻辑, 从而允许使用不同的构造器来初始化Java对象
- 使用 this 调用另外一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句. 使用 this 调用重载的构造器时, 系统会根据 this 后括号里面的实参来调用形参列表与之对应的构造器
// 例子1: 构造器的重载
public class ConstructorOverlaod
{
public String name;
public int count;
// 提供无参数的构造器
public ConstructorOverlaod(){}
// 提供两个参数的构造器
// 对该构造器返回的对象执行初始化
// 重载: 同类同方法名不同形参列表
public ConstructorOverlaod(String name, int count)
{
this.name = name;
this.count = count;
}
public static void main(String[] args)
{
// 通过无参数构造器创建ConstructorOverlaod对象
ConstructorOverlaod oc1 = new ConstructorOverlaod();
// 通过有参数构造器创建ConstructorOverlaod对象
ConstructorOverlaod oc2 = new ConstructorOverlaod("jefxff", 29);
System.out.println(oc1.name + " " + oc1.count);
System.out.println(oc2.name + " " + oc2.count);
}
}
// 例子2: 使用 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;
}
}
/**
* 说明:
* 1. 上面的Apple类里包含了三个构造器, 其中第三个构造器通过 this 来调用另一个重载构造
* 器的初始化代码.程序中 this(name,color);调用表明用该类另外一个带两字符串参数的
* 构造器.
*/
6. 类的继承
6.1 继承的特点
- Java的继承通过 extends 实现,实现继承的类被称之为子类,被继承的类被称之为父类
- Java的继承具有单继承的特点,每个子类只有一个直接父类,间接父类可以有无限多个
- 父类和子类的关系是一种一般和特殊的关系,子类是对父类的扩展;如: 水果和苹果
- 因为子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大
- Java的子类不能获得父类的构造器
- 定义Java类的时候,无显式指定Java直接父类的,这个类的默认父类是 java.lang.Object 类,因此 java.lang.Object 是所有类的父类,要么直接父类,要么间接父类; 所有的 Java 对象都可以调用 java.lang.Object 类所定义的实例方法
- 子类角度讲: 子类扩展(或继承)了父类; 父类角度讲: 父类派生了子类,描述的是同一件事,只是观察的角度不同
语法格式
修饰符 class SubSlass extends SuperClass
{
// 类定义部分
}
// 例子:
// 定义一个水果类
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 对象
Apple a = new Apple();
// 因为 Apple 对象本身没有 weight 成员变量
// 因为 Apple 的父类是有 weight 成员变量的,也可以访问 Apple 对象的 weight 成员变量
a.weight = 65;
// 调用 Apple 对象的 info() 方法
a.info();
}
}
// 间接继承的例子
public class Fruit extends Plant{...}
public class Apple extends Fruit{...}
// 例子中Fruit类是Apple类的直接父类,Plant类是Fruit类的直接父类, 所以Plant类是Apple类的
// 间接父类
6.2 重写父类的方法
- 子类包含与父类同名方法的现象叫做方法的重写(Override),也叫做方法覆盖
- 覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法
- 子类覆盖了父类的方法后,子类的对象将无法访问父类中被覆盖的方法,但是可以在子类方法中调用父类中被覆盖的方法。
- 子类调用父类被覆盖的方法,则可以使用 super (被覆盖的是实例方法)或者使用父类名(被覆盖的是类方法)作为调用者调用父类中被覆盖的方法
- 父类中 private 修饰的方法,子类无法重写;即使子类按照重写格式重写了父类中 private 修饰
的方法,依然不是重写,是子类重新定义的一个新方法
遵循的规则:(两同两小一大)
- "两同": 方法名相同,形参列表相同
- "两小": 第一是子类方法返回值类型应比父类方法返回值类型更小或相等; 第二是子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
- "一大": 指的是子类方法的访问权限应比父类方法的访问权限更大或者相等
重写(override)和重载(overload)的区别:
- 重载主要发生在同一个类的多个同名方法之间,重写发生在子类和父类的同名方法之间
- 都发生在方法之间,都要求方法名相同
- 父类方法和子类方法之间的重载:子类获得了父类方法,如果子类定义了一个与父类方法有相同的方法名,但形参列表不同的方法,就会形成父类方法和子类方法的重载
// 例子:
// 重写父类方法的例子
public class Bird
{
// Bird类的fly()方法
pubilc void fly()
{
System.out.println("我在天空自由的飞翔...");
}
}
// 再定义一个 Ostrich 类
public class Ostrich extends Bird
{
// 重写父类的方法
public void fly()
{
System.out.println("我只能奔跑...");
}
public static void main(String[] args)
{
// 创建一个 Ostrich 对象
Ostrish os = new Ostrich();
// 执行 Ostrich 对象的 fly() 方法, 输出: "我只能奔跑..."
os.fly();
// 在子类方法中通过 super 显式的调用父类被覆盖的实例方法
super.fly();
}
}
6.3 super 限定
- 用于需要在子类中调用父类被覆盖的实例方法,则可使用 super 限定来调用父类被覆盖的实例方法
- 如果被覆盖的是类变量,在子类的方法中则可以通过父类名作为调用者来访问被覆盖的类变量
- super 不能出现在 static 修饰的方法中,static 修饰的方法属于类,该方法的调用者可能是一个类,而不是一个对象,因而 super 限定也就失去了意义
- 如果在构造器中使用 super, 则 super 用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量
- 如果子类定义了和父类同名的是变量,则会发生子类实例变量隐藏父类实例变量的情形,正常情况下子类里定义的方法直接访问该实例变量默认会访问到子类中定义的实例变量,无法访问父类中被隐藏的实例变量。 在子类定义的实例方法中可以通过 super 来访问父类的实例变量
- 如果子类里没有包含和父类同名的实例变量,那么在子类实例方法中访问成员变量时,无需显式的使用 super 或父类名作为调用者
- 如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量不是完全覆盖,因此在系统创建子类对象时,依然会为父类中定义的,被隐藏的变量分配内存空间
无显式指定调用者,某方法中访问成员变量 a 时查找的顺序:
- 查找该方法中是否有名 a 的局部变量
- 查找当前类中是否包含名为 a 的成员变量
- 查找 a 的直接父类中是否包含名为 a 的成员变量,依次上溯 a 的所有父类,直到 java.lang.Object 类,如果最终不能找到名为 a 的成员变量,则系统报编译出错误
// 例子1:
// 子类定义的实例方法通过 super 来访问父类中被隐藏的实例变量
public 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(); // out: 7
sc.accessBase(); // out: 5
}
}
// 程序说明:
// 1. 上面程序的 BaseClass 和 SubClass 中都定义了名为 a 的实例变量,则 SubClass 的
// a 实例变量将会隐藏 BaseClass 的 a 实例变量。
// 2. 当系统创建了 SubClass 对象的时候,实际上会为 SubClass 对象分配两块内存,一块用
// 于存储在 SubClass 类中定义的 a 实例变量, 一块用于存储从 BaseClass 类中继承
// 得到的 a 实例变量。
// 3. System.out.println(super.a); 这句代码中当访问 super.a 时,此时使用 super 限
// 定该实例从父类继承得到的 a 实例变量, 而不是在当前类中定义的 a 变量
// 例子2:
// 子类中定义父类同名的实例变量并不会完全覆盖父类中定义的实例变量,而只是简单隐藏
class Parent
{
public String tag = "java"; // ①
}
class Dericed extends Parent
{
// 定义一个私有的 tag 实例变量来隐藏父类的 tag 实例变量
private String tag = "I Love Java"; // ②
}
public class HideTest
{
public static void main(String[] args)
{
Dericed d = new Dericed();
// 程序不可访问 d 的私有变量 tag (因为 private 修饰的变量或者方法,只能在当前
// 类中使用) 所以下面的这句代码将引起编译错误
// System.out.println(d.tag); // ③
// 将 d 变量显式地向上转型(强制类型转换)为 Parent 后,即可访问 tag 实例变量
System.out.println(((Parent)d).tag); // ④
}
}
// 程序说明:
// 1. 上面程序的①处代码为父类Parent定义了一个 tag 的实例变量,②处的代码为其子类定义了一个
// private 的 tag 实例变量,子类中定义的这个实例变量将会隐藏父类中定义的 tag 实例变量
// 2. 程序的入口 main() 类中先创建了一个 Dericed 对象, 这个 Dericed 对象将会保存两个 tag
// 实例变量, 一个是在 Parent 中定义的 tag 实例变量, 一个是在 Dericed 中定义的 tag
// 实例变量,此时程序中还包含一个 d 变量,它引用一个 Dericed 对象
// 3. 程序将 Dericed 对象赋给 d 变量,当在③出的代码试图通过d来访问tag 实例变量时,程序将
// 提示访问权限不允许,这是因为访问哪个实例变量由声明该变量的类型决定,所以系统将会试图
// 访问在②处定义的 tag 实例变量;程序在④处的代码先将d变量强制向上转型为Parent类型,
// 再通过它来访问 tag 实例变量是允许的,因为此时系统将会访问在①处定义的 tag 实例变量
// 也就会输出 “java”
6.4 调用父类的构造器
- 子类不会获得父类的构造器,但是子类构造器里可以调用父类构造器的初始化代码, 类似于前面学的一个构造器调用另一个重载的构造器
- 在一个构造器中调用另一个重载的构造器使用 this 调用来完成, 在子类构造器中调用父类的构造器使用 super 调用来完成
- 使用 super 调用父类构造器也必须出现在子类构造器执行体的第一行, 所以 this 调用和 super 调用不可以同时出现
- 不管是否使用 super 调用来执行父类构造器的初始化代码, 子类构造器总是调用父类构造器一次
- 创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行, 最后才执行本类的构造器; 如果某个类通过 this 调用了同类中重载的构造器, 就会依次执行此父类的多个构造器
子类构造器调用父类构造器情形:
- 子类构造器执行体的第一行使用 super 显式的调用父类构造器, 系统将根据 super 调用里传入的实参列表调用父类对应的构造器
- 子类构造器执行体的第一行代码使用 this 显式调用本类中重载的构造器, 系统将根据 this 调用里传入的实参列表调用本类中的另一个构造器, 执行本类中另外一个构造器时即会调用父类的构造器
- 子类构造器执行体中既没有 super 调用, 也没有 this 调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
//例子1:
// 子类通过 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 调用来调用父类构造器初始化过程
super(size, name);
this.color = color;
}
piblic static void main(String[] args)
{
Sub a = new Sub(5.6, "测试对象", "红色");
// 输出 Sub 对象的三个实例变量
System.out.println(a.size + " --- " + s.name + " --- " + s.color);
}
}
//例子2:
// 定义三个类,他们之间有严格的继承关系,了解构造器之间的调用关系
public Creature
{
public Creature()
{
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature
{
public Animal(String name)
{
System.out.println("Animal 带一个参数的构造器, " + "该动物的name为 " + name);
}
public Animal(String name, int age)
{
// 使用 this 调用同一个重载的构造器
this(name);
System.out.println("Animal带有两个参数的构造器, " + " 其 age 为 " + age);
}
}
public class Wolf extends Animal
{
public Wolf()
{
// 显式调用父类有两个参数的构造器
super("灰太狼", 3);
System.out.println("Wolf无参数的构造器");
}
public static void main(Srting[] args)
{
new Wolf();
}
}
// 程序说明:
// 1. 程序输出:
// creature 无参数的构造器
// Animal 带有一个参数的构造器, 该动物的 name 为灰太狼
// Animal 带有两个参数的构造器, 其 age 为 3
// Wolf 无参数构造器
7. 多态
7.1 多态性
- Java引用变量有两个类型:一个是编译时类型, 一个是运行时类型. 编译时类型由声明该变量时使用的类型决定, 运行时类型由实际赋给该变量的对象决定. 如果编译时类型和运行时类型不一致, 就可能出现所谓的多态(Polymorphism)
- 因为子类是一种特殊的父类, 因此Java允许把一个子类对象直接赋给一个父类引用变量, 无须任何类型的转换, 或者被称之为向上转型(upcasting), 向上转型由系统自动完成.
- 相同类型的变量, 调用同一个方法时呈现出多种不同的行为特征,就是多态
- 引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法,所以编写 Java 代码时,引用变量只能调用声明变量时所用类里包含的方法。例如: 通过 Object p = new Person(); 代码定义了一个变量 p, 则这个 p 只能调用 Object类的方法,而不能调用 Person 类里定义的方法
- 通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量
// 例子:
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();
}
}
7.2 引用变量的强制类型转换
- 引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法,即使它实际所引用的对象确实包含该方法
- 如果需要让引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时的类型
- 基本类型之间转换只能在数值类型之间进行,包括(整数类型,字符型,浮点型),但是数值类型和布尔类型之间不能进行类型转换
- 引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,如:试图将一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型为子类类型), 否则报 ClasscaCas-tException 异常
- 在进行强制转换之前,应先通过 instanceof 运算符来判断是否可以成功转换,如下面例子2
- 当把一个子类对象赋给父类引用变量时,被称为向上转型(upcasting), 这种转型总是可以成功,这种转型表明这个引用变量的编译时类型时父类,但实际执行他的方法时,依然表现出子类对象的行为方式;但是把一个父类对象赋给子类引用变量时,就需要进行强制类型转换,而且还可能在运行时产生ClasscaCastException异常
用法
(type)variable
// 说明: 这种用法可以将 variable 变量转换成一个 type 类型的变量
// 例子1:
// 强制类型转换的例子
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
// 所以下面的代码运行时引发ClasscaCastException异常
// String str = (String)objPri;
}
}
// 例子2:
if (objPri instanceof String)
{
String str = (String)objPri;
}
7.3 instanceof 运算符
- instanceof 运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口),用于判断前面的对象是否是后面的类,或者其子类,实现类的实例,如果是,返回true,否则返回false
- instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么是后面的类具有父子继承关系,否则会引发编译错误
- instanceof 的作用是:在进行强制类型转换之前,首先判断前一个对象是否是后一个类的实例,是否可成功转换,先通过 instanceof 判断是否可以转换, 再使用 type 运算符进行强转
// 例子:
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)); // 返回 ture
// Math 与 Object 类存在继承关系,可以进行 instanceof 运算, 返回 false
System.out.println("字符串是否是 Math 类的实例:"
+ (hello instanceof Math)); // 返回 false
// String 实现了 Comparable 接口, 所以返回 true
System.out.println("字符串是否是 Comparable 接口的实例:"
+ (hello instanceof Comparable)); // 返回 ture
String a = "hello";
// String 类和 Math 类没有继承关系, 所以下面的代码编译无法通过
System.out.println("字符串是否是 Math 类的实例:"
+ (hello instanceof Math )); // 编译报错
}
}
8. 继承与组合
8.1 使用继承的注意点
- 子类扩展父类以后,如果访问权限允许,子类可以直接访问父类的成员变量和方法, 相当于子类可以直接调用父类成员变量和方法
- 继承严重破坏了父类的封装性
设计父类的原则:
- 尽量隐藏父类的内部数据,尽量把父类的所有成员变量设置成 private 访问类型, 不要让子类直接访问父类的成员变量
- 不要让子类可以随意访问, 修改父类的方法. 如果父类中的方法需要被外部类调用,则必须使用public 修饰, 但又不希望子类重写该方法, 可以使用 final 修饰符来修饰; 如果希望父类的某个方法被子类重写, 但不希望被其他类自由访问, 则使用 protected 来修饰该方法
- 尽量不要在父类构造器中调用将要被子类重写的方法; 使用 private 修饰的父类的构造器,子类无法调用父类的构造器,也就无法继承该类
派生子类的条件
- 子类需要额外增加属性, 而不仅仅是属性值的改变
- 子类需要增加自己独有的行文方式(包括增加新的方法或重写父类的方法)
// 例子:
class Base
{
public Base()
{
test();
}
public void test() // ① 号 test() 方法
{
System.out.println("将被子类重写的方法");
}
}
public class Sub extends Base
{
private String name;
public void test() // ② 号 test() 方法
{
System.out.println("子类重写父类的方法,"
+ "其name字符串长度为" + name.length());
}
public static void main(String[] args)
{
// 下面代码将会引发空指针异常
Sub s = new Sub();
}
}
// 程序说明:
// 1. 当系统试图创建 Sub 对象时,同样会先执行其父类构造器, 如果父类构造器调用了被
// 其子列重写的方法, 则变成调用被子类重写后的方法.
// 2. 当创建 Sub 对象时, 会先执行 Base 类中的 Base 构造器, 而 Base 构造器中调用了
// test() 方法----并不是调用①号 test() 方法, 而是调用②号 test() 方法, 此时
// Sub 对象的 name 实例变量时 null, 因此将引发空指针异常
8.2 利用组合实现复用
- 如果需要复用一个类, 除了把这个类当作基类来继承之外,还可以把该类当成另一个类的组合成分, 从而允许新类直接复用该类的 public 方法. 不管是继承还是组合, 都允许在新类(对于继承就是子类)中直接复用旧类的方法
- 组合是把旧类对象作为新类的成员变量组合进来, 用以实现新类的功能,用户看到的是新类的方法, 而不能看到被组合对象的方法
- 子类和组合关系里的整体类, 都可以复用原有类的方法, 用于实现自身的功能
- 继承要表达的是一种"是(is-a)"的关系, 而组合表达的是"有(has-a)"的关系
// 例子1:
// 继承关系的三个类
class Animal
{
// private 修饰的方法子类无法继承
private void beat()
{
System.out.println("嗯! 心脏还在跳动....");
}
public void breath()
{
beat();
System.out.println("吸一口气, 呼一口气, 呼吸中.....");
}
}
// 继承Animal,直接复用父类的 breath()方法
class Bird extends Animal
{
public void fly()
{
System.out.println("我是可以在天空中飞翔的....");
}
}
// 继承Animal,直接复用父类的 breath()方法
class Wolf extends Animal
{
public void run()
{
System.out.println("我是在陆地上跑的那个狼....");
}
}
public class InheriTest
{
public static void main(String[] args)
{
Bird b = new Bird();
b.breath();
b.fly();
Wolf w = new Wolf();
w.breath();
w.run();
}
}
// 例子2:
// 组合关系的三个类
class Animal
{
// private 修饰的方法子类无法继承
private void beat()
{
System.out.println("嗯! 心脏还在跳动....");
}
public void breath()
{
beat();
System.out.println("吸一口气, 呼一口气, 呼吸中.....");
}
}
class Bird
{
// 将原来的父类组合到原来的子类, 作为子类的一个组合成分
private Animal a;
public Bird(Animal a)
{
this.a = a;
}
// 重新定义一个自己的breath()方法
public void breath()
{
// 直接调用Animal提供的breath()方法来实现Bird的breath()方法
a.breath();
}
public void fly()
{
System.out.println("我是可以在天空中飞翔的....");
}
}
class Wolf
{
// 将原来的父类组合到原来的子类,作为子类的一个组合成分
private Animal a;
public Wolf(Animal a)
{
this.a = a;
}
// 重新定义一个自己的breath()方法
public void breath()
{
// 直接调用Animal提供的breath()方法来实现Wolf的breath()方法
a.breath();
}
public void run()
{
System.out.println("我是在陆地上跑的那个狼....");
}
}
public class CompositeTest
{
public static void main(String[] args)
{
// 此时需要显式创建被组合对象
Animal a1 = new Animal();
Bird b = new Bird(a1);
b.breath();
b.fly();
// 此时需要显式创建被组合对象
Animal a2 = new Animal();
Wolf w = new Wolf(a2);
w.breath();
w.run();
}
}
9. 初始化块
9.1 使用初始化块
- 初始化块是Java类里可能出现的第4种成员(前面学的成员变量,方法和构造器), 一个类里可以有多个初始化块,相同类型的初始化块之间有顺序: 前面定义的初始化块先执行,后面定义的初始化块后执行
- 初始化块的修饰符只能是 staitc , 使用 static 修饰的初始化块被称之为静态初始化块.
- 初始化块里的代码可以包含任何可执行性语句, 包括定义局部变量, 调用其他对象的方法, 以及使用分支语句, 循环语句
- 初始化块和构造器的作用非常相似, 他们都用于对Java对象执行指定的初始化操作; 但是二者之间还是有一定的差异: 普通初始化块,声明实例变量指定的默认值都可认为是对象的初始化代码, 他们的执行顺序与源码中排列的顺序相同
初始化块语法
[修饰符]{
// 初始化块的可执行性代码
// ...
}
// 例子1:
// Person.java
public class Person
{
// 定义一个普通初始化块
{
int a = 6;
if (a > 4)
{
System.out.println("Person初始化块: a = " + a + " 局部变量a的值大于4");
}
System.out.println("Person 的第一个初始化块");
}
// 定义第二个初始化块
{
System.out.println("Person的第二个初始化块");
}
// 定义一个无参数的构造器
public Person()
{
System.out.println("Person 类的无参数构造器 ");
}
public static void main(String[] args)
{
new Person();
}
}
// 程序说明:
// 1. 当创建 Java 对象时, 系统总是先调用该类里定义的初始化块, 如果一个类里定义了2个普通
// 初始化块, 则前面定义的初始化块先执行, 后面定义的初始化块后执行
// 2. 初始化块虽然是 Java 类的一种成员, 但它没有名字, 也就没有标识, 因此无法同类, 对象
// 来调用初始化块.
// 3. 初始化块只在创建 Java 对象时隐式执行,总是全部执行(所以可以把多个初始化块合并成一
// 个初始化块, 让代码更简洁), 而且在执行构造器之前执行
// 例子2:
// InstanceInitTest.java
public class InstanceInitTest
{
// 先执行初始化块将变量 a 实例变量赋值为6
{
int a = 6;
}
// 再执行将 a 谁变量赋值为 9
int a = 9;
pbulic static void main(String[] args)
{
// 下面的代码将输出9
System.out.println(new InstanceInitTest().a);
}
}
// 程序说明:
// 1. 当Java创建一个对象时, 系统先为该对象的所有实例变量分配内存(前提是该类已经加载过了)
// 接着程序开始对这些实例变量执行初始化
// 2. 初始化的顺序是: 先执行初始化块或声明实例变量时指定的初始值(这两个地方指定的执行允
// 许与他们在源代码中排序相同的顺序), 在执行构造器里指定的初始值
9.2 初始化块和构造器
- 也可以讲, 初始化块是构造器的补充,初始化块总是在构造器执行之前执行
- 初始化块不接受参数, 所以初始化块对同一个类的所有对象所进行的初始化处理完全相同
- 基于上面两点,就可以把不接受任何参数,初始化处理对所有对象完全相同的代码提取为初始化块
- 假象: 实际上初始化块是一个假象, 使用 javac 命名编译 java 类后, 该 Java类中的初始化块会消失 --- 初始化块中代码会被"还原"到每个构造器中, 且位于构造器所有代码的前面
9.3 静态初始化块
- 使用static修饰初始化块, 被称之为静态初始化块, 也被称之为类初始化块(普通初始化块负责对对象执行初始化, 类初始化块负责对类进行初始化)
- 系统将在类初始化阶段执行初始化块,而不是在创建对象时才执行
- 静态初始化块总是比普通初始化块先执行
- 静态初始化块通常用于对类变量执行初始化, 静态初始化块不能对实例变量进行初始化处理(静态初始化块不能访问非静态成员,包括不能访问实例变量和实例方法)
- Java系统加载并初始化某个类时, 总是保证该类的所有父类(包括直接父类和间接父类)全部加载并初始化
// 例子:
// 创建三个类(都提供静态初始化块和普通初始化块)
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();
}
}
// 程序说明:
// 1. 第一次创建一个 Leaf 对象时, 因为系统中还不存在 Leaf 类, 因此需要先加载并初始化
// Leaf 类, 初始化 Leaf 类时会先执行其顶层父类的静态初始化块, 再执行其直接父类的
// 静态初始化块, 最后才执行 Leaf 本身的静态初始化块
// 2. 一旦 Leaf 类初始化成功后, Leaf 类在该虚拟机里将一直存在, 因此当第二次创建 Leaf
// 实例时无须再次对 Leaf 类进行初始化
// 3. 每一次创建一个 Leaf 对象时, 都需要先执行最顶层父类的初始化块, 构造器, 然后执行其
// 父类的初始化块, 构造器.....最后才执行Leaf类的初始化块和构造器
// 4. 静态初始化块和声明静态成员变量时所指定的初始值都是该类的初始化代码, 他们的执行
// 顺序与源码中排列顺序相同. 如下面例子2
// 例子2:
// StaticInitTest.java
public class StaticInitTest{
// 先执行静态初始化块将 a 静态成员变量赋值为 6
static{
a = 6;
}
// 再将 a 静态成员变量赋值为 9
static int a = 9;
public static void main(String[] args){
// 下面的代码输出 9
System.out.println(StaticInitTest.a);
}
}
// 程序说明:
// 1. 上面程序两次对 a 静态成员变量进行赋值, 执行结果是 a 值是9,这表明 static int a = 9;
// 这行代码位于静态初始化块之后执行.
// 2. 当 JVM 第一次主动使用某个类时, 系统会在类准备阶段为该类的所有静态成员变量分类内存,
// 在初始化揭短则负责初始化这些静态成员变量, 初始化静态成员变量就是执行类初始化代码
// 或者声明类成员变量时指定的初始值, 他们的执行顺序与源码中排列的顺序相同