文章目录
- 1.封装(掌握)
- 1.1 封装的理解
- 1.2 不封装存在的问题
- 1.3 怎么封装
- 1.4 难点解惑
- 1.5 练习
- 2. this 和 static
- 2.1 this(掌握)
- 2.1.1 this是什么
- 2.1.2 this 在实例方法中使用
- 2.1.3 this访问实例变量
- 2.1.4 this扩展①
- 2.1.5 this扩展②
- 2.1.6 this总结
- 2.1.7 this在构造方法中使用
- 2.2 static(掌握)
- 2.2.1 static概述
- 2.2.2 静态变量
- 2.2.2.1 静态变量使用“引用”访问?
- 2.2.2.2 静态方法使用“引用”访问?
- 2.2.3 静态代码块
- 2.2.3.1 语法格式与介绍
- 2.2.3.2 静态代码块中访问静态变量
- 2.2.4 静态方法
- 2.3 难点解惑
- 2.4 练习
- 2.4.1 选择题
- 2.4.2 判断下面代码的输出结果,并说明原因
- 2.4.3 找出下面代码的错误,并说明为什么错了
1.封装(掌握)
1.1 封装的理解
封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。 系统的其他对象只能通过包裹在数据外面的已经授权的操作来与这个封装的对象进行交流和交互。也就是说用户是无需知道对象内部的细节,但可以通过该对象对外提供的接口来访问该对象。
现实世界事物封装举例:
- “鼠标”,外部有一个壳,将内部的原件封装起来,至于鼠标内部的细节是什么,我们不需要关心,只需要知道鼠标对外提供了左键、右键、滚动滑轮这三个简单的操作。对于用户来说只要知道左键、右键、滚动滑轮都能完成什么功能就行了。为什么鼠标内部的原件要在外部包装一个“壳”呢?起码内部的原件是安全的。
- “数码相机”,外部也有一个壳,将内部复杂的结构包装起来,对
外提供简单的按键,这样每个人都可以很快的学会照相了,因为它的按键很简单,另外照相机内部精密的原件也受到了壳儿的保护,不容易坏掉。
封装的好处:
封装之后就形成了独立实体,独立实体可以在不同的环境中重复使用,显然封装可以降低程序的耦合度,提高程序的扩展性,以及重用性或复用性,例如“鼠标”可以在 A 电脑上使用,也可以在 B 电脑上使用。另外封装可以隐藏内部实现细节,站在对象外部是看不到内部复杂结构的,对外只提供了简单的安全的操作入口,所以封装之后,实体更安全了。
1.2 不封装存在的问题
如下代码:
class MobilePhone {
//电压:手机正常电压在 3~5V
double voltage;
}
public class MobilePhoneTest {
public static void main(String[] args) {
MobilePhone phone = new MobilePhone();
phone.voltage = 3.7;
System.out.println("手机电压 = " + phone.voltage);
phone.voltage = 100;
System.out.println("手机电压 = " + phone.voltage);
}
}
以上程序 MobilePhone 类未进行封装,其中的电压属性 voltage 对外暴露,在外部程序当中可以对 MobilePhone 对象的电压 voltage 属性进行随意访问,导致了它的不安全,例如手机的正常电压是 3~5V,但是以上程序已经将手机电压设置为 100V,这个时候显然是要出问题的,但这个程序编译以及运行仍然是正常的,没有出现任何问题,这是不对的。
1.3 怎么封装
为了保证内部数据的安全,这个时候就需要进行封装了,封装的第一步就是将应该隐藏的数据隐藏起来,起码在外部是无法随意访问这些数据的,可以使用 java 语言中的 private 修饰符,private 修饰的数据表示私有的,私有的数据只能在本类当中访问。
class MobilePhone {
//电压:手机正常电压在 3~5V
private double voltage;
}
public class MobilePhoneTest {
public static void main(String[] args) {
MobilePhone phone = new MobilePhone();
phone.voltage = 3.7;
System.out.println("手机电压 = " + phone.voltage);
phone.voltage = 100;
System.out.println("手机电压 = " + phone.voltage);
}
}
直接编译报错:
通过以上的测试,手机对象的电压属性确实受到了保护,在外部程序中无法访问了。但从
当前情况来看,voltage 属性有点儿过于安全了,一个对象的属性无法被外部程序访问,自然这
个数据就没有存在的价值了。所以这个时候就需要进入封装的第二步了:对外提供公开的访问入口,让外部程序统一通过这个入口去访问数据,我们可以在这个入口处设立关卡,进行安全
控制,这样对象内部的数据就安全了。
对于“一个”属性来说,我们对外应该提供几个访问入口呢?通常情况下我们访问对象的
某个属性,不外乎读取(get)和修改(set),所以对外提供的访问入口应该有两个,这两个
方法通常被称为 set 方法和 get 方法(请注意:set 和 get 方法访问的都是某个具体对象的属性,
不同的对象调用 get 方法获取的属性值不同,所以 set 和 get 方法必须有对象的存在才能调用,
这样的方法定义的时候不能使用 static 关键字修饰,被称为实例方法。实例方法必须使用“引
用”的方式调用。 还记得之前我们接触的方法都是被 static 修饰的,这些方法直接采用“类名”
的方式调用,而不需要创建对象,在这里显然是不行的)。请看以下代码:
class MobilePhone {
//电压:手机正常电压在 3~5V
private double voltage;
public MobilePhone(){
}
public void setVoltage(double _voltage){
if(_voltage < 3 || _voltage > 5){
//当电压低于 3V或者高于 5V时抛出异常,程序则终止
throw new RuntimeException("电压非法,请爱护手机!");
}
//程序如果能执行到此处说明以上并没有发生异常,电压值合法
voltage = _voltage;
}
public double getVoltage(){
return voltage;
}
}
public class MobilePhoneTest {
public static void main(String[] args) {
MobilePhone phone = new MobilePhone();
phone.setVoltage(3.7);
System.out.println("手机电压 :" + phone.getVoltage());
phone.setVoltage(100);
System.out.println("手机电压 :" + phone.getVoltage());
}
}
运行结果:
通过以上程序,可以看出 MobilePhone 的 voltage 属性不能在外部程序中随意访问了,只能调用 MobilePhone 的 setVoltage()方法来修改电压,调用 getVoltage()方法来读取电压,在setVoltage()方法中编写了安全控制代码,当电压低于 3V,或者高于 5V 的时候,程序抛出了异常,不允许修改电压值,程序结束了。只有合法的时候,才允许程序修改电压值。(后续可以通过学习异常机制对异常进行处理)
总之,在 java 语言中封装的步骤应该是这样的:
- 需要被保护的属性使用 private 进行修饰;
- 给这个私有的属性对外提供公开的 set 和 get 方法,其中 set 方法用来修改属性的值,get 方法用来读取属性的值。
- set 和 get 方法在命名上也是有规范的: set 方法名是 set + 属性名(属性名首字母大写),get 方法名是 get + 属性名(属性名首字母大写)。
- set 方法有一个参数,用来给属性赋值,set 方法没有返回值,一般在 set 方法内部编写安全控制程序,因为毕竟 set 方法是修改内部数据的。
- get 方法不需要参数,返回值类型是该属性所属类型。
- 【先记住】 set 方法和 get 方法都不带 static 关键字,不带 static 关键字的方法称为实例方法,这些方法调用的时候需要先创建对象,然后通过“引用”去调用这些方法,实例方法不能直接采用“类名”的方式调用。
示例代码:
class Product{
private int no;
private String name;
private double price;
public Product(){
}
public Product(int _no, String _name, double _price){
no = _no;
name = _name;
price = _price;
}
public void setNo(int _no){
no = _no;
}
public int getNo(){
return no;
}
public String getName() {
return name;
}
public void setName(String _name) {
name = _name;
}
public double getPrice() {
return price;
}
public void setPrice(double _price) {
price = _price;
}
}
public class ProductTest {
public static void main(String[] args) {
Product p1 = new Product(10000 , "小米 12" , 3599.0);
System.out.println("商品编号:" + p1.getNo());
System.out.println("商品名称:" + p1.getName());
System.out.println("商品单价:" + p1.getPrice());
p1.setNo(70000);
p1.setName("小米 14");
p1.setPrice(5999);
System.out.println("商品编号:" + p1.getNo());
System.out.println("商品名称:" + p1.getName());
System.out.println("商品单价:" + p1.getPrice());
}
}
运行结果:
补充:构造方法中给属性赋值是在创建对象的时候完成的,当对象创建完毕之后,属性可能还是会被修改的,后期要想修改属性的值,这个时候就必须调用 set 方法了。
1.4 难点解惑
理解:为什么在进行封装时,提供的 setter 和 getter 方法就不能添加 static 关键字了呢?
这块内容在后期学习 static 关键字之后自然就好理解了,先简单解释一下:带有 static 关键字的方法是静态方法,不需要创建对象,直接通过“类”来调用。对于没有 static 关键字的方法被称为实例方法,这些方法执行时要求必须先创建对象,然后通过“引用”的方式来调用。而对于封装来说,setter 和 getter 方法都是访问对象内部的属性,所以 setter 和 getter 方法在定义时不能添加 static 关键字。
1.5 练习
第一题:定义“人”类,“人”类包括这些属性:姓名、性别、年龄等。使用封装的方式编写
程序,要求对年龄进行安全控制,合法的年龄范围为[0~100],其他值表示不合法。
class People {
private String name;
private char sex;
private int age;
public People() {
}
public People(String _name, char _sex, int _age) {
name = _name;
sex = _sex;
age = _age;
}
public String getName() {
return name;
}
public void setName(String _name) {
name = _name;
}
public char getSex() {
return sex;
}
public void setSex(char _sex) {
sex = _sex;
}
public int getAge() {
return age;
}
public void setAge(int _age) {
if(_age < 0 || _age > 100){
//当年龄不合法则抛出异常,程序则终止
throw new RuntimeException("非法年龄!");
}
age = _age;
}
}
public class PeopleTest{
public static void main(String[] args) {
People p1 = new People("李宗盛", '男', 23);
System.out.println(p1.getName());
System.out.println(p1.getSex());
System.out.println(p1.getAge());
p1.setAge(101);
}
}
2. this 和 static
2.1 this(掌握)
2.1.1 this是什么
先看一段代码:
class Customer {
private String name;
public Customer(){
}
public Customer(String _name){
name = _name;
}
public void setName(String _name){
name = _name;
}
public String getName(){
return name;
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
Customer rose = new Customer("rose");
}
}
以上代码的this内存结构图为:
this
可以看做一个变量,它是一个引用,存储在 Java 虚拟机堆内存的对象内部,this
这个引用保存了当前对象的内存地址指向自身,任何一个堆内存的 java 对象都有一个 this
,也就是说创建 100 个 java 对象则分别对应 100 个this
。通过以上的内存图,可以看出jack 引用
保存的内存地址是0x1111
,对应的this 引用
保存的内存地址也是 0x1111
,所以jack 引用
和this 引用
是可以划等号的。也就是说访问对象的时候 jack.name
和 this.name
是一样的,都是访问该引用所指向对象的 name
属性。
this
指向当前对象
,也可以说this
代表当前对象
,this
可以使用在实例方法中以及构造方法中,语法格式分别为this.xx
和this(xx)
。
2.1.2 this 在实例方法中使用
this 不能出现在带有 static 的方法当中,如下:
public class ThisInStaticMethod {
public static void main(String[] args) {
ThisInStaticMethod.method();
}
public static void method(){
System.out.println(this);
}
}
编译报错:
通过以上的测试得知 this不能出现在 static的方法当中,原因如下:
首先static 的方法,在调用的时候是不需要创建对象的,直接采用“类名”的方式调用,也就是说static 方法执行的过程中是不需要“当前对象”参与的,所以 static 的方法中不能使用 this,因为 this 代表的就是“当前对象”。
之前的“封装”学习中,曾编写属性相关的 set 和 get 方法,set 和 get方法在声明的时候不允许带 static 关键字,我们把这样的方法叫做实例方法,与实例变量一样,实例变量和实例方法都是对象相关,必须有对象的存在,然后通过“引用”去访问。
将 set 和 get 方法设计为实例方法的原因: 因为 set 和 get 方法操作的是实例变量,“不同的对象”调用 get 方法最终得到的数据是不同的,例如 zhangsan 调用 getName()方法得到的名字是zhangsan,lisi 调用 getName()方法得到的名字是 lisi,显然 get 方法是一个对象级别的方法,不能直接采用“类名”调用,必须先创建对象,再通过“引用”去访问。
this 可以出现在实例方法当中,因为实例方法在执行的时候一定是对象去触发的,实例方法一定是对象才能去调用的,而 this 恰巧又代表“当前对象”,所以 “谁”去调用这个实例方法 this 就是“谁” 。请看以下代码:
class Customer {
private String name;
public Customer(){
}
public Customer(String _name){
name = _name;
}
public void setName(String _name){
name = _name;
}
public String getName(){
return name;
}
public void shopping(){
System.out.println("shopping() --> " + this);
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
System.out.println("main() ---> " + jack);
jack.shopping();
System.out.println("====================");
Customer rose = new Customer("rose");
System.out.println("main() ---> " + rose);
rose.shopping();
}
}
运行结果:
分析一下:this
代表当前对象,jack调用shopping()
方法,this
就是jack,rose调用shopping方法,this
就是rose。
由上可知:this 可以使用在实例方法当中,它指向当前正在执行这个动作的对象。
2.1.3 this访问实例变量
实例变量怎么访问?正规的访问方式是采用引用.
去访问。请看下面的代码:
class Customer {
private String name;
public Customer(){
}
public Customer(String _name){
name = _name;
}
public void setName(String _name){
name = _name;
}
public String getName(){
return name;
}
public void shopping(){
System.out.println(name + " is shopping!");
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
jack.shopping();
Customer rose = new Customer("rose");
rose.shopping();
}
}
运行结果:
将以上部分代码片段取出来进行分析:
class Customer {
private String name; //实例变量
...
public void shopping(){
//jack 调用 shopping,当前对象是 jack
//rose 调用 shopping,当前对象是 rose
//name 是实例变量,不用“引用”可以访问?(以上结果表示可以)
System.out.println(name + " is shopping!");
//正规的访问方式应该是“引用.name”,比如
//System.out.println(jack.name + " is shopping!");
//或者
//System.out.println(rose.name + " is shopping!");
//对不起,jack 和 rose 在 main 方法当中,在这里不可见,不能用
//难道是这样???
System.out.println(this.name + " is shopping!");
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
jack.shopping();
Customer rose = new Customer("rose");
rose.shopping();
}
}
完整修改代码:
class Customer {
private String name;
public Customer(){
}
public Customer(String _name){
name = _name;
}
public void setName(String _name){
name = _name;
}
public String getName(){
return name;
}
public void shopping(){
System.out.println(name + " is shopping!");
System.out.println(this.name + " is shopping!");
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
jack.shopping();
System.out.println("=======================");
Customer rose = new Customer("rose");
rose.shopping();
}
}
运行结果:
通 过 以 上 的 测 试 我 们 得 知 :
System.out.println(name + " is shopping!")
等价于System.out.println(this.name + " is shopping!")
。也就是说在 shopping()
这个“实例方法”当中直接访问“实例变量”name
就表示访问当前对象的name
。换句话说 在实例方法中可以直接访问当前对象的实例变量,而"this."
是可以省略的。
"this."
什么时候不能省略呢?以上name=_name
这样的代码其实可以修改为更优雅的样子,如下:
public class Customer {
private String name;
public Customer(){
}
public Customer(String name){
this.name = name;//这里的“this.”不能省略
}
public void setName(String name){
this.name = name;//这里的“this.”不能省略
}
public String getName(){
return name; //这里的“this.”可以省略
}
public void shopping(){
//这里的“this.”可以省略
System.out.println(name + " is shopping!");
}
}
以上代码当中this.name = name
,其中this.name
表示实例变量name
,等号右边的name
是局部变量 name
,此时如果省略"this."
,则变成name = name
,这两个 name
都是局部变量(java 遵守就近原则),和实例变量 name
无关了,显然是不可以省略"this."
的。
2.1.4 this扩展①
如下代码:
class Customer {
private String name;
public Customer(){
}
public Customer(String name){
this.name = name;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
//实例方法
public void shopping(){
System.out.println(name + " is shopping!");
System.out.println(name + " 选好商品了!");
//pay()支付方法是实例方法,实例方法需要使用“引用”调用
//那么这个“引用”是谁呢?
//当前对象在购物,肯定是当前对象在支付,所以引用是this
this.pay();
//同样“this.”可以省略
pay();
}
//实例方法
public void pay(){
System.out.println(name + "支付成功!");
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
jack.shopping();
System.out.println("=======================");
Customer rose = new Customer("rose");
rose.shopping();
}
}
运行结果:
通过以上的测试,可以看出在一个实例方法当中可以直接去访问其它的实例方法,方法是对象的一种行为描述,实例方法中直接调用其它的实例方法,就表示“当前对象”完成了一系列行为动作。例如在实例方法 shopping()中调用另一个实例方法 pay(),这个过程就表示 jack 在选购商品,选好商品之后,完成支付环节,其中选购商品是一个动作,完成支付是另一个动作。
2.1.5 this扩展②
public class ThisTest {
int i = 10;
public static void main(String[] args) {
System.out.println(i);
}
}
编译报错:
原因分析:首先i
变量是实例变量,实例变量要想访问必须先创建对象,然后通过“引用”去访问,main方法是 static 的方法,也就是说 main 方法是通过“类名”去调用的,在 main 方法中没有“当前对象”的概念,也就是说 main 方法中不能使用 this,所以编译报错了。可以修改如下:
public class ThisTest {
int i = 10;
public static void main(String[] args) {
//这肯定是不行的,因为 main 方法带有 static,不能用 this
//System.out.println(this.i);
//可以自己创建一个对象
ThisTest thisTest = new ThisTest();
//通过引用访问
System.out.println(thisTest.i);
}
}
运行结果:
结论:在 static
的方法中不能直接访问实例变量,要访问实例变量必须先自己创建一个对象,通过引用
可以去访问,不能通过 this
访问,因为在 static
方法中是不能存在 this
的。其实这种设计也是有道理的,因为 static
的方法在执行的时候是采用“类名”去调用,没有对象的参与,自然也不会存在当前对象,所以 static
的方法执行过程中不存在 this
。
在 static 方法中能够直接访问实例方法吗?请看以下代码:
public class ThisTest {
public static void main(String[] args) {
doSome();
}
public void doSome(){
System.out.println("do some...");
}
}
编译报错:
原因分析:因为实例方法必须先创建对象,通过引用去调用,在以上的 main 方法中并没有创建对象,更没有 this。所以在 main 方法中无法直接访问实例方法。结论就是:在 static 的方法中不能直接访问实例方法。修改的话,同样是先创建对象:
public class ThisTest {
public static void main(String[] args) {
ThisTest t = new ThisTest();
t.doSome();
}
public void doSome(){
System.out.println("do some...");
}
}
运行结果:
2.1.6 this总结
this
不能出现在static
的方法中,可以出现在实例方法中,代表当前对象。- 多数情况下
this
是可以省略不写的,但是在区分局部变量和实例变量的时候不能省略。 - 在实例方法中可以直接访问当前对象实例变量以及实例方法,在
static
方法中无法直接访问实例变量和实例方法。
2.1.7 this在构造方法中使用
this
的另外一种用法,在构造方法第一行(只能出现在第一行,这是规定,记住就行),通过当前构造方法调用本类当中其它的构造方法,其目的是为了代码复用。 调用时的语法格式是:this(实际参数列表)
,请看以下代码:
class Date {
private int year;
private int month;
private int day;
//业务要求,默认创建的日期为 1970 年 1 月 1 日
public Date(){
this.year = 1970;
this.month = 1;
this.day = 1;
}
public Date(int year,int month,int day){
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
}
public class DateTest {
public static void main(String[] args) {
Date d1 = new Date();
System.out.println(d1.getYear() + "年 " + d1.getMonth() + "月 " +
d1.getDay() + "日");
Date d2 = new Date(2008 , 8, 8);
System.out.println(d2.getYear() + "年 " + d2.getMonth() + "月 " +
d2.getDay() + "日");
}
}
运行结果:
对于无参数构造方法和有参数构造方法,观察发现:
可以在无参数构造方法中使用this(实际参数列表);
来调用有参数的构造方法,这样就可以让代码得到复用了,请看:
最终运行结果同上。
在 this()上一行尝试添加代码,请看代码截图以及编译结果:
通过以上测试得出:this()语法只能出现在构造方法第一行。
2.2 static(掌握)
2.2.1 static概述
static 是 java 语言中的关键字,表示“静态的”,它可以用来修饰变量、方法、代码块等,修饰的变量叫做静态变量,修饰的方法叫做静态方法,修饰的代码块叫做静态代码块。在 java语言中凡是用 static 修饰的都是类相关的,不需要创建对象,直接通过“类名”即可访问,即使使用“引用”去访问,在运行的时候也和堆内存当中的对象无关。
2.2.2 静态变量
java 中的变量包括:局部变量和成员变量。
- 在方法体中声明的变量为局部变量,有效范围很小,只能在方法体中访问,方法结束之后局部变量内存就释放了,在内存方面局部变量存储在栈当中。
- 在类体中定义的变量为成员变量,成员变量又包括实例变量和静态变量。
- 没有使用 static 关键字声明的变量称为实例变量,实例变量是对象级别的,每个对象的实例变量值可能不同,所以实例变量必须先创建对象,通过“引用”去访问。
- 使用static 关键字声明的成员变量称为静态变量,静态变量访问时不需要创建对象,直接通过“类名”访问。
- 实例变量存储在堆内存当中,静态变量存储在方法区当中。
- 实例变量在构造方法执行过程中初始化,静态变量在类加载时初始化。
对于何时要将成员变量声明为静态变量,请看以下代码:
class Man {
//身份证号
int idCard;
//性别(所有男人的性别都是“男”)
//true 表示男,false 表示女
boolean sex = true;
public Man(int idCard){
this.idCard = idCard;
}
}
public class ManTest {
public static void main(String[] args) {
Man jack = new Man(100);
System.out.println(jack.idCard + "," + (jack.sex ? "男" : "女"));
Man sun = new Man(101);
System.out.println(sun.idCard + "," + (sun.sex ? "男" : "女"));
Man cok = new Man(102);
System.out.println(cok.idCard + "," + (cok.sex ? "男" : "女"));
}
}
运行结果:
以上程序的内存结构图:
“男人类”创建的所有“男人对象”,每一个“男人对象”的身份证号都不一样,该属性应该每个对象持有一份,所以应该定义为实例变量,而每一个“男人对象”的性别都是“男”,不会随着对象的改变而变化,性别值永远都是“男”,这种情况下,就没有必要让每一个“男人对象”持有一份,否则会浪费大量的堆内存空间,所以这个时候建议将“性别=男”属性定义为类级别的属性,声明为静态变量,上升为“整个族”的数据,这样的变量不需要创建对象直接使用“类名”即可访问。
修改如下:
class Man {
//身份证号
int idCard;
//性别(所有男人的性别都是“男”)
//true 表示男,false 表示女
static boolean sex = true; //声明为静态变量
public Man(int idCard){
this.idCard = idCard;
}
}
public class ManTest {
public static void main(String[] args) {
Man jack = new Man(100);
System.out.println(jack.idCard + "," + (Man.sex ? "男" : "女"));
Man sun = new Man(101);
System.out.println(sun.idCard + "," + (Man.sex ? "男" : "女"));
Man cok = new Man(102);
System.out.println(cok.idCard + "," + (Man.sex ? "男" : "女"));
}
}
运行结果同上,再看看内存结构图:
小结: 当一个类的所有对象的某个“属性值”不会随着对象的改变而变化的时候,建议将该属性定义为静态属性(或者说把这个变量定义为静态变量),静态变量在类加载的时候初始化,存储在方法区当中,不需要创建对象,直接通过“类名”来访问。
2.2.2.1 静态变量使用“引用”访问?
如果静态变量使用“引用”来访问,可以吗,如果可以的话,这个访问和具体的对象有关系吗?来看以下代码:
class Man {
//身份证号
int idCard;
//性别(所有男人的性别都是“男”)
//true 表示男,false 表示女
static boolean sex = true; //声明为静态变量
public Man(int idCard){
this.idCard = idCard;
}
}
public class ManTest {
public static void main(String[] args) {
//静态变量比较正式的访问方式
System.out.println("性别 = " + Man.sex);
//创建对象
Man jack = new Man(100);
//使用“引用”来访问静态变量可以吗?
System.out.println("性别 = " + jack.sex);
//对象被垃圾回收器回收了
jack = null;
//使用“引用”还可以访问吗?
System.out.println("性别 = " + jack.sex);
}
}
运行结果:
通过以上代码以及运行结果可以看出,静态变量也可以使用“引用”去访问,但实际上在执行过程中,“引用”所指向的对象并没有参与,如果是空引用访问实例变量,程序一定会发生空指针异常,但是以上的程序编译通过了,并且运行的时候也没有出现任何异常,这说明虽然表面看起来是采用“引用”去访问,但实际上在运行的时候还是直接通过“类”去访问的。
2.2.2.2 静态方法使用“引用”访问?
静态方法也是如此,请看如下代码:
class Man {
//身份证号
int idCard;
//性别(所有男人的性别都是“男”)
//true 表示男,false 表示女
static boolean sex = true;
public Man(int idCard){
this.idCard = idCard;
}
//静态方法
public static void printInfo(){
System.out.println("-----" + (Man.sex ? "男" : "女") + "------");
}
}
public class ManTest {
public static void main(String[] args) {
//静态变量比较正式的访问方式
System.out.println("性别 = " + Man.sex);
//创建对象
Man jack = new Man(100);
//使用“引用”来访问静态变量可以吗?
System.out.println("性别 = " + jack.sex);
//对象被垃圾回收器回收了
jack = null;
//使用“引用”还可以访问吗?
System.out.println("性别 = " + jack.sex);
//静态方法比较正式的访问方式
Man.printInfo();
//访问静态方法可以使用引用吗?并且空的引用可以吗?
jack.printInfo();
}
}
运行结果:
通过以上代码测试得知,静态变量和静态方法比较正式的方式是直接采用“类名”访问,但实际上使用“引用”也可以访问,并且空引用访问静态变量和静态方法并不会出现空指针异常。实际上,在开发中并不建议使用“引用”去访问静态相关的成员,因为这样会让程序员困惑,因为采用“引用”方式访问的时候,程序员会认为你访问的是实例相关的成员。
小结:
- 所有实例相关的,包括实例变量和实例方法,必须先创建对象,然后通过“引用”的方式去访问,如果空引用访问实例相关的成员,必然会出现空指针异常。
- 所有静态相关的,包括静态变量和静态方法,直接使用“类名”去访问。虽然静态相关的成员也能使用“引用”去访问,但这种方式并不被主张。
2.2.3 静态代码块
2.2.3.1 语法格式与介绍
类{
//静态代码块
static{
java 语句;
}
}
静态代码块在类加载时执行,并且只执行一次。其实际上是 java 语言为程序员准备的一个特殊的时刻,这个时刻就是类加载时刻,如果你想在类加载的时候执行一段代码,那么这段代码就有的放矢了。例如我们要在类加载的时候解析某个文件,并且要求该文件只解析一次,那么此时就可以把解析该文件的代码写到静态代码块当中了。我们来测试一下静态代码块:
public class StaticTest {
//静态代码块
static{
System.out.println(2);
}
//静态代码块
static{
System.out.println(1);
}
//main 方法
public static void main(String[] args) {
System.out.println("main execute!");
}
//静态代码块
static{
System.out.println(0);
}
}
运行结果:
通过以上的测试可以得知:
- 一个类当中可以编写多个静态代码块(尽管大部分情况下只编写一个),并且静态代码块遵循自上而下的顺序依次执行,所以有的时候放在类体当中的代码是有执行顺序的(大部分情况下类体当中的代码没有顺序要求,方法体当中的代码是有顺序要求的,方法体当中的代码必须遵守自上而下的顺序依次逐行执行);
- 静态代码块当中的代码在 main 方法执行之前执行,这是因为静态代码块在类加载时执行,并且只执行一次。
2.2.3.2 静态代码块中访问静态变量
再看一下以下代码:
public class StaticTest02 {
int i = 100;
public static void main(String[] args) {
System.out.println("main execute!");
}
static{
System.out.println(i);
}
}
编译报错:
原因分析:
因为 i 变量是实例变量,实例变量必须先创建对象才能访问,静态代码块在类加载时执行,这个时候对象还没有创建呢,所以 i 变量在这里是不能这样访问的。可以考虑在 i 变量前添加 static,这样 i 变量就变成静态变量了,静态变量访问时不需要创建对象,直接通过“类”即可访问,如下:
public class StaticTest02 {
static int i = 100;
public static void main(String[] args) {
System.out.println("main execute!");
}
static{
System.out.println("静态变量:i="+i);
}
}
运行结果:
若代码修改为如下:
public class StaticTest02 {
static{
System.out.println("静态变量 i = " + i);
}
static int i = 100;
public static void main(String[] args) {
System.out.println("main execute!");
}
}
编译报错:
通过测试,可以看到有的时候类体当中的代码也是有顺序要求的(类体当中定义两个独立的方法,这两个方法是没有先后顺序要求的),静态代码块在类加载时执行,静态变量在类加载时初始化,它们在同一时间发生,所以必然会有顺序要求,如果在静态代码块中要访问 i 变量,那么 i 变量必须放到静态代码块之前。
2.2.4 静态方法
方法在什么情况下会声明为静态的呢?方法实际上描述的是行为动作,当某个动作在触发的时候需要对象的参与,这个方法应该定义为实例方法,例如:
- 每个玩篮球的人都会打篮球,但是你打篮球和科比打篮球最终的效果是不一样的,显然打篮球这个动作存在对象差异化,该方法应该定义为实例方法。
- 每个高中生都有考试的行为,但是你考试和学霸考试最终的结果是不一样的,一个上了“家里蹲大学”,一个上了“清华大学”,显然这个动作也是需要对象参与才能完成的,所以考试这个方法应该定义为实例方法。
以上描述是从设计思想角度出发来进行选择,其实也可以从代码的角度来进行判断,当方法体中需要直接访问当前对象的实例变量或者实例方法的时候,该方法必须定义为实例方法,因为只有实例方法中才有 this,静态方法中不存在 this。 请看代码:
class Customer {
String name;
public Customer(String name){
this.name = name;
}
public void shopping(){
//直接访问当前对象的 name
System.out.println(name + "正在选购商品!");
//继续让当前对象去支付
pay();
}
public void pay(){
System.out.println(name + "正在支付!");
}
}
public class CustomerTest {
public static void main(String[] args) {
Customer jack = new Customer("jack");
jack.shopping();
Customer rose = new Customer("rose");
rose.shopping();
}
}
运行结果:
在以上的代码中,不同的客户购物,最终的效果都不同,另外在 shopping()方法中访问了当前对象的实例变量 name,以及调用了实例方法 pay(),所以 shopping()方法不能定义为静态方法,必须声明为实例方法。
在实际的开发中,“工具类”当中的方法一般定义为静态方法,因为工具类就是为了方便大家的使用,将方法定义为静态方法,比较方便调用,不需要创建对象,直接使用类名就可以访问。请看以下工具类,为了简化System.out.println();
代码而编写的工具类:
class U {
public static void p(int data){
System.out.println(data);
}
public static void p(long data){
System.out.println(data);
}
public static void p(float data){
System.out.println(data);
}
public static void p(double data){
System.out.println(data);
}
public static void p(boolean data){
System.out.println(data);
}
public static void p(char data){
System.out.println(data);
}
public static void p(String data){
System.out.println(data);
}
}
public class HelloWorld {
public static void main(String[] args) {
U.p("Hello World!");
U.p(true);
U.p(3.14);
U.p('A');
}
}
运行结果:
2.3 难点解惑
对于 static 来说,代码的执行顺序是一个需要大家注意的地方,一般来说方法体当中的代码是有执行顺序要求的,之前所接触的程序中,类体当中的程序没有顺序要求,但自从认识了 static 之后,我们发现类体当中的代码也有执行顺序的要求了,尤其是 static 修饰的静态变量,以及static修饰的静态代码块他们是有先后顺序的,这里需要记住的是:static修饰的静态变量以及静态代码块都是在类加载时执行,并且遵循自上而下的顺序依次逐行执行。
2.4 练习
2.4.1 选择题
public class Test {
static int value = 9;
public static void main(String[] args) throws Exception{
new Test().printValue();
}
public void printValue(){
int value = 69;
System.out.println(this.value);
}
}
A. 编译错误
B. 打印 9
C. 打印 69
D. 运行时抛出异常
运行结果:
理解: 方法中的int value = 69;
是局部变量,而 static int value = 9;
是实例变量,方法中的System.out.println(this.value);
调用的是实例变量!
2.4.2 判断下面代码的输出结果,并说明原因
class User {
private String name;
public User(){
}
public void setName(String name){
name = name;
}
public String getName(){
return name;
}
}
public class UserTest {
public static void main(String[] args) {
User user = new User();
user.setName("zhangsan");
System.out.println(user.getName());
}
}
最终的输出结果是:null。
原因:setName 方法体当中的 name = name 是把局部变量 name 赋值给局部变量 name,和实例变量 name 无关,所以 getName()方法获取的实例变量值是 null。
2.4.3 找出下面代码的错误,并说明为什么错了
public class Test {
int i;
static int j;
public void m1(){
System.out.println(i);
System.out.println(j);
m2();
m3();
}
public void m2(){
}
public static void m3(){
System.out.println(i); //报错,因为静态方法中无法直接访问实例变量 i
System.out.println(j);
m2(); //报错,因为静态方法中无法直接访问实例方法 m2()
m4();
}
public static void m4(){
}
}