1 final 关键字
1.1 final修饰变量
1.1.1 final概述
final单词直译为“最终的“,在Java中可以用来修饰变量、方法和类:
- final修饰的变量:可以初始化,不能再更改
- final修饰的方法:不能在子类中重写
- final修饰的类:不能再被继承派生出子类了
1.1.2 final修饰局部变量
在方法中声明的变量称为局部变量。
局部变量加final修饰以后,只能初始化一次,不能再次更改。因此 final 修饰的变量可以在声明的同时初始化或者在构造函数中初始化。
注意:当引用类型变量被设置为final时候,表示其值就是引用对象的地址值不能再次修改了,但是此时被引用对象的属性或者元素是可以被修改的。
使用final的目的是保护局部变量的值不变,避免在方法运算期间被意外篡改。如果一个需要反复更改的局部变量就不要使用final修饰。
查看如下所示代码:
int 类型的变量a添加了 final 修饰符,则被初始化之后将不能被再次赋值。
另外, final 修饰的变量 arr 是int类型的数组,也是只能被初始化一次不能再次被修改。但是因为变量 arr 是引用类型,其值是数组对象的首地址(初始化以后不能再次被修改),即变量arr与数组之间的引用关系不能发生改变了;但是数组的内容是可以改变的。因此可以给数组的元素赋值,但是不能给 arr 重新定义。
1.1.3【案例】final修饰局部变量示例
编写代码,测试 final 修饰的基本类型变量和引用类型变量。
案例示意代码如下:
import java.util.Arrays;
public class FinalDemo1 {public static void main(String[] args) {final int a;a = 8; //第一次为变量赋值,称为初始化System.out.println(a); ////a = 9; //编译错误,a不能再次被修改final int b = 9; //声明变量直接初始化System.out.println(b);//b = 10; //编译错误,不能再次修改final变量//final修饰的引用类型变量://引用类型变量初始为一个地址值后不能再次修改final int[] arr = {5, 6};//arr中存储的数组地址不能再次修改类//arr引用不能修改,但是被引用对象的内容可以修改arr[0] = 9;System.out.println(Arrays.toString(arr)); //[9, 6]//不可以更改arr变量的值,因为arr是final类型的!//arr = new int[8];final Ball ball = new Ball();ball.d = 10;System.out.println(ball.d);//不能更换ball的值,也就是地址值//ball = null;}
}
class Ball{int d = 5;
}
1.1.4 final修饰方法的参数
Java中方法参数也是一种局部变量,只是其声明位置是方法参数,在接收到传递参数的时候初始化。
在方法参数上可以使用final修饰。final修饰以后也是初始化以后不能再次修改,由于方法参数是在调用方法传递参数值时候初始化的,所以在方法运行期间方法参数变量的值不能修改了。
使用final修饰方法参数的好处也是保护变量的值,避免在方法运行期间参数变量的值被意外篡改。
查看如下代码示例:
方法 test 的第二个参数被声明为 final,在方法内部不能再修改其数值了。
1.1.5【案例】final修饰方法参数示例
定义带两个参数的方法(其中一个参数用 final 修饰),编写代码,测试 final 修饰的参数的使用。
案例示意代码如下:
public class FinalDemo2 {public static void main(String[] args) {test(5,6);test(7,8);}public static void test(int a, final int b) {a = 9;//b = 8; //编译错误,不能再次更改变量bSystem.out.println("a:"+a+",b:"+b);}
}
1.1.6 final修饰实例变量
在类中声明的对象属性,由于是属于每个对象实例的变量所以也称为“实例变量”。
final可以修饰实例变量,在final修饰实例变量时候必须直接初始化或者在构造器中初始化,并且实例变量也是在初始化以后不能再次改变了。
使用final修饰实例变量的目的也是保护实例变量,使其值在初始化以后不能改变,避免程序的意外篡改。比如如果希望一个对象的唯一ID编号,在初始化以后不能改了,就可以利用final修饰。
在实际开发中很少使用final修饰的实例变量!主要原因是不方便对象的复用。很多Java底层框架都会利用对象池重复使用对象,避免反复创建销毁对象的性能开销,如果对象属性是final的,就无法再次进行赋值重用对象了!
查看如下代码示例:
类 Eoo 的实例变量 a 和 b,都声明为 final,在 main 方法中测试发现,创建 Eoo 对象后,可以访问该属性,但是不能修改它。
1.1.7【案例】final修饰实例变量示例
为类定义 final 修饰的实例变量并编写代码测试其特点。
案例示意代码如下:
public class FinalDemo3 {public static void main(String[] args) {Eoo eoo = new Eoo(8);System.out.println(eoo.a); //5System.out.println(eoo.b); //8//不能再次更改final的实例变量//eoo.a = 9;//eoo.b = 10;Eoo e2 = new Eoo(10);System.out.println(e2.a);System.out.println(e2.b);}
}
class Eoo {//final的属性必须初始化final int a = 5;final int b;public Eoo(int b) {this.b = b;}
}
1.2 final修饰方法
1.2.1 final修饰方法
方法上可以使用final修饰,final的方法在子类中不能被重写修改了。简单理解:final方法不能被重写。final修饰的方法不会影响方法在当前类中的使用,但是如果派生了子类,则在子类中不能重写修改父类中定义的final方法。
final方法的好处是避免被子类使用重写语法修改方法的功能,保护方法的功能是“最终”版本。如果需要保护方法的功能,避免在子类中重写修改,就可以使用final进行声明。
但是在实际工程项目中很少使用final方法,原因是很多框架工具都会采用“动态代理”技术代理(重写)对象的功能,实现灵活的软件功能,如果使用final的方法将直接影响这些框架功能!很多些软件开发企业在编程规范中明确规定:不能声明final方法!
查看如下示例:
类 Foo 中的方法 test() 添加了 final 修饰符,可以被子类继承但是不能被子类重写。
1.2.2【案例】final修饰方法示例
为类定义 final 修饰的方法并编写代码测试其特点。
案例示意代码如下:
public class FinalDemo4 {public static void main(String[] args) {Foo foo = new Foo();foo.test();SubFoo sf = new SubFoo();sf.test();}
}
class Foo{public final void test() {System.out.println("Foo.test()");}
}
class SubFoo extends Foo{//public void test() { //编译错误,不能重写Foo中的final方法// System.out.println("SubFoo.test()");//}
}
1.3 final修饰类
1.3.1 final修饰类
类名也可以使用final修饰,被final修饰的类将不能再派生子类了,也就是终结了类的继承。简单理解:final类不能被继承。
final类的好处和final方法类似,也是可以避免被继承和重写,避免被子类修改功能。Java的很多核心API,都是final类型,这样就保护了这些非常重要的API功能。这些API包括:String、Math、Integer、Double、Long等。这些API都不能派生子类。
在开发中也不允许使用final声明类,原因也是因为声明final类以后,造成很多框架无法采用“动态代理”技术代理扩展对象的功能,很多些软件开发企业在编程规范中明确规定:不能声明final类!
查看如下示例:
类Goo添加了 final 修饰符,则不能被继承。
1.3.2【案例】final修饰类示例
定义 final 修饰的类,并编写代码测试其特点。
案例示意代码如下:
public class FinalDemo5 {public static void main(String[] args) {Goo goo = new Goo();goo.test();}
}
final class Goo{public void test() {System.out.println("test()");}
}
//class SubGoo extends Goo{ //编译错误,不能继承final类
//}
2 static 关键字
2.1 静态变量
2.1.1 成员变量
作为类的成员,在类体中声明的变量称为成员变量,成员变量有三种:
- 一种是实例变量:是属于每个对象属性,每个对象中都有一份
- 一种是静态变量:是属于类的变量,只有一份,全体对象共享的同一份变量
- 一种是常量:是不变的常数
成员变量示例如下:
2.1.2 静态变量
使用static修饰类的成员变量,称为静态变量。静态变量只有一份,是可以被全体对象共享的一份变量,这点与实例变量有着巨大的差异。实例变量是每个对象实例都有一份的变量。而静态变量是无论用类创建多少对象,都始终唯一的变量。
静态变量和类的信息一起存储在方法区,是属于类的变量。
可以用静态变量存储程序中只有一个就够的数据,比如:办公软件项目中的公司名称和公司LOGO,地图应用中的各类图标,只需要加载一份就可以了。多份相同的数据反而会浪费大量的时间和存储空间。
2.1.3 静态变量的访问
因为静态变量是属于类的变量,所以使用“类名.变量名”访问,在类的内部可以省略类名。
静态变量关键点:
- 静态变量只有一份,可以被全体对象共享
- 软件中只有一份的数据应该使用static修饰
查看如下示例:
类Foo定义了静态变量 b,使用 Foo.b 访问。
2.1.4【案例】静态变量示例
在类中定义静态变量,并编写代码测试其特点。
案例示意代码如下:
public class StaticDemo1 {public static void main(String[] args) {Soo s1 = new Soo();Soo s2 = new Soo();s1.a = 8;s2.a = 10;Soo.b = 11; //使用类名访问静态变量System.out.println(s1.a + "," + s1.b); //8,11System.out.println(s2.a + "," + s2.b); //10,11System.out.println(Soo.b); //11 读取静态变量}
}
class Soo{int a; //实例变量、对象属性static int b; //静态变量
}
2.1.5 静态变量工作原理
在面试中经常有问起静态变量工作原理的。简单的说就是静态变量在类加载期间在方法区中分配,静态变量是属于类的变量。
静态变量的具体工作原理是:
1、Java源文件经过编译得到字节码文件,每个类编译为一个class文件
2、当执行Java程序时候,每用到一个类Java就会自动将对应的字节码加载到方法区
- 创建对象时候会自动加载类
- 访问类的静态属性时候会自动加载类
- 执行类中的静态方法时候会自动加载类
- 字节码文件只加载一次
3、如果类中有静态变量,Java就会在加载类期间将其在方法区中分配出来,静态变量也初始化一次,只有一份
4、创建对象时候按照类中声明的实例变量分配对象的属性,每创建一个对象,就会分配一组对象属性。
工作原理如下图所示:
2.1.6 static final
Java中同时使用static final关键字声明“常量”,常量用于声明不会变化的量。比如:数学中的圆周率PI、自然常数e,物理学中的光速C,都是常量。在软件开发中也会将固定不变的数值声明为常量。比如程序中给用户的提示信息,一般是固定值,就可以声明为常量。 如下所示:
public class Message {public static final String MSG_LOGINFAILED = “用户名或密码错误”;public static final String MSG_LOGINSUCC = “登录成功”;
}
2.1.7 常量的细节
软件中不能改变的数据,应该都定义为常量 :同时使用static final修饰,两个关键字的顺序可以调换。
常量必须初始化,其命名建议都是大写字母,多个单词使用下划线隔开,比如 MAX_VALUE。
Java API中提供了很多常量:
- Math.PI、 Math.E
- Integer.MAX_VALUE、Integer.MIN_VALUE
- Long.MAX_VALUE、Long.MIN_VALUE
2.2 静态方法
2.2.1 静态方法
所谓的静态方法就是在方法声明时候添加static关键字。添加了静态关键字的方法称为静态方法,静态方法是属于类的方法。属于类的方法就可以直接使用类名直接引用方法。比如:Math.random()就是一个静态方法。
查看如下示例:
对于 Person 类的 add() 方法,它没有用到当前的属性,则可以定义为静态方法。在使用时,使用类名.方法名() 的方式调用。
2.2.2 静态方法的细节
当一个方法其方法体中没有用到任何当前对象的属性时候,则此方法就可以定义为静态方法。相反如果方法用到了当前对象的数据,就不能定义为静态方法。这也是是否在方法前面添加static关键字的原则。由于静态方法与对象数据无关,所以静态方法可以使用类名直接访问。
静态方法和对象方法还有一个区别就是,静态方法没有隐含的局部变量this。而对象方法中是保护隐含局部变量this,对象方法就是通过这个this引用访问了当前对象的属性和方法。由于这个差别,就有一个现象:静态方法不能访问实例变量和对象方法。
因为main方法也是静态方法,所以main方法也不能访问当前类型的实例变量和对象方法。
2.2.3【案例】静态方法示例
在类中定义静态方法,并编写代码测试其特点。
案例示意代码如下:
public class StaticDemo2 {public static void main(String[] args) {Person.add(7, 8); //用类名调用静态方法Person tom = new Person("Tom");tom.whoru(); //用对象引用调用对象的方法}
}
class Person{String name;public Person(String name) {this.name = name;}public void whoru() {//对象方法中包含隐含局部变量thisSystem.out.println("我是"+this.name);}//如果方法中没有用到当前对象的属性/方法就声明为staticpublic static void add(int a, int b) {//静态方法中没有隐含局部变量thisSystem.out.println(a+b);}
}
2.3 static 其他用法
2.3.1 代码块
在类中可以使用{}定义代码块,代码块在创建对象时候按照顺序执行,其功能与构造器类似,可以用于初始化对象属性。代码示意如下:
class Cell{int a;{//代码块,在创建对象时候执行a = (int)(Math.random()*8);}
}
大多情况下优先使用构造器初始化对象属性,代码块很少被使用,了解即可。
2.3.2 静态代码块
类中使用static修饰的代码块称为静态代码块。
静态代码块在类加载期间执行,因为Java类只加载一次,所以静态代码块也只执行一次。就是因为这个特性,经常用静态代码块初始化类中的静态属性。如:将图片资源定义为静态属性,然后利用静态代码块加载图片文件,初始化静态图片属性。
静态代码块的使用示例如下:
2.3.3【案例】静态代码块示例
定义并测试静态代码块,案例示意代码如下:
public class StaticDemo3 {public static void main(String[] args) {//Java会在创建对象之前自动加载类CircleCircle c1 = new Circle();Circle c2 = new Circle();System.out.println(Circle.angle);}
}
class Circle{static double angle; //角static {//静态代码块,在类加载期间执行,只执行一次System.out.println("初始化angle");angle = Math.PI * 2;}
}
2.3.4 静态导入
Java 8 提供了静态导入语法,用于简化静态资源的编码,其语法为:import static。例如:
import static java.lang.Math.PI;
import static java.lang.Math.sin;
import static java.lang.Math.*;
使用示例如下:
3 abstract 抽象
3.1 抽象类
3.1.1 什么是抽象类
使用抽象关键字abstract声明的类是抽象类,抽象类不能直接实例化创建对象。
这个定义看上去非常茫然,究其原因是因为在面向对象设计时候,会利用“泛化”将子类的共同属性和方法抽取出来设计出父类,此时的父类往往是半成品类,只包含部分属性和方法,甚至属性值都没有合理初始化,如下图所示:
如果直接创建对象并且使用有可能造成各种不理想结果,甚至是异常故障。
可以用抽象父类来解决这个问题。
3.1.2 抽象类示例
为便于理解抽象类的作用,我们先开发一个不使用抽象类的案例,查看此时可能存在的问题。
/*** Person类的作用是为子类提供代码复用*/
public class Person {String name;int age;public void whoru() {System.out.println("我是"+name);}
}
package day07.abstract01;
public class Student extends Person{public Student(String name, int age) {this.name = name;this.age = age;}public void study() {System.out.println("学习");}
}
package day07.abstract01;
public class Teacher extends Person{public Teacher(String name, int age) {this.name = name;this.age = age;}public void teach() {System.out.println("讲课");}
}
package day07.abstract01;
public class Worker extends Person{public Worker(String name, int age) {this.name = name;this.age = age;}public void work() {System.out.println("工作");}
}
package day07.abstract01;
public class AbstractDemo1 {public static void main(String[] args) {Student s = new Student("Tom", 12);Teacher t = new Teacher("Andy", 28);Worker w = new Worker("Jerry", 28);s.whoru();s.study();t.whoru();t.teach();w.whoru();w.work();//问题:如果能够直接创建Person对象,其方法运算结果不理想Person p = new Person();p.whoru();}
}
上面案例的运行结果如下所示:
上述运行结果中创建Person类型对象,调用其whoru()方法,得到结果是null。
这个结果并不理想,就像生活中问一个人是谁,回答“佚名”一样。造成这个结果的原因是:泛化出来的父类Person 是个半成品类,创建其对象后,其name属性没有合理初始化,导致输出了name的默认值 null。
如何解决上述问题呢?需要明确表示 Person是一个半成品类,不能直接实例化。
因此,就可以使用abstract修饰一下Person类型,使其成为抽象类,这样Java的编译器就会限制Person类型,不允许其直接实例化创建对象。不能被实例化,就不可能造成后续结果了。所以合理使用abstract类,可以避免创建不应该创建的对象,减少程序的错误。
将上述案例中的Person类声明为抽象类,可避免出现创建半成品实例的情况。案例代码示意如下:
/*** Person类的作用是为子类提供代码复用*/
public abstract class Person {String name;int age;public void whoru() {System.out.println("我是"+name);}
}
package day07.abstract02;
public class AbstractDemo2 {public static void main(String[] args) {Student s = new Student("Tom", 12);Teacher t = new Teacher("Andy", 28);Worker w = new Worker("Jerry", 28);s.whoru();s.study();t.whoru();t.teach();w.whoru();w.work();//问题:如果能够直接创建Person对象,其方法运算结果不理想//Java编译器检查,不允许创建抽象类型的对象!//Person p = new Person();//p.whoru();}
}
3.1.3 抽象类不可以实例化
面向对象设计时候根据子类泛化得到的半成品父类,应该定义为抽象类,这样可以限制创建半成品类的对象,减少意外的错误发生。
正因为抽象类不可以被实例化,因此 abstract 和 final 不可以同时修饰一个类:final 关键字使得类不可被继承,而抽象类既不能能被继承,又不能被实例化,则没有任何意义。
使用抽象类时:
1、在类名前面添加abstract关键字以后就是抽象类了
2、抽象类可以作为父类被子类继承,可以定义变量
3、抽象类不能直接创建对象
3.2 抽象方法
3.2.1 什么是抽象方法
使用abstract关键字声明,不包含方法体的方法称为抽象方法。
这个定义同样晦涩,究其原因是因为在利用泛化设计父类时候,有这种情况全体子类都有相同的方法,但是每个具体方法实现都并不相同,这样只能将方法名抽取到父类,方法体留在每个子类中,这种只有方法名称的方法,就是抽象方法。
3.2.2 抽象方法的语法
抽象方法的语法:
1、使用abstract声明方法,不能有方法体
2、包含抽象方法的类必须声明为抽象类,因为包含抽象方法的类一定是不完整的半成品类
3、子类继承抽象类时候必须重写(实现)抽象方法,否则出现编译错误
- 可以将抽象方法看作父类对子类的行为约定,必须被子类重写实现
实际使用时,如何选择抽象方法?建议规则:每个子类都有,但是每个子类实现都不同的方法泛化为抽象方法!
3.2.3【案例】抽象方法示例
定义抽象类,并包含抽象方法;定义子类继承自抽象类,并重写抽象方法。编写代码测试其特点。
案例示意代码如下:
/*** Person类的作用是为子类提供代码复用* 设计为抽象类,只能被继承,不能创建对象*/
public abstract class Person {String name;int age;public void whoru() {System.out.println("我是"+name);}/*** 日程计划*/public abstract void schedule();
}
package day07.abstract03;
public class Student extends Person {public Student(String name, int age) {this.name = name;this.age = age;}public void study() {System.out.println("学习");}public void schedule() {System.out.println("吃饭、听课");}
}
package day07.abstract03;
public class Student extends Person {public Student(String name, int age) {this.name = name;this.age = age;}public void study() {System.out.println("学习");}public void schedule() {System.out.println("吃饭、听课");}
}
package day07.abstract03;
public class Worker extends Person {public Worker(String name, int age) {this.name = name;this.age = age;}public void work() {System.out.println("工作");}public void schedule() {System.out.println("吃饭、开车");}
}
package day07.abstract03;
public class AbstractDemo3 {public static void main(String[] args) {Student s = new Student("Tom", 12);Teacher t = new Teacher("Andy", 28);Worker w = new Worker("Jerry", 28);s.schedule();t.schedule();w.schedule();}
}
3.2.4 抽象类的意义
综合抽象类和抽象方法的讲解,可以简单理解抽象类的意义如下:
- 为其子类提供一个公共的类型
- 封装子类中的重复内容(成员变量和方法)
- 定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的