Java(三)面向对象、封装继承多态、重写和重载、枚举
面向对象、类和对象、封装继承多态、抽象类和接口、重写和重载、构造方法、this、super、static、final、枚举
文章目录
本系列文章:
Java(一)数据类型、修饰符、运算符
Java(二)分支循环、数组、字符串、方法
Java(三)面向对象、重写和重载、枚举
Java(四)内部类、包装类、异常、日期
Java(五)反射、克隆、泛型、元注解
Java(六)IO、NIO、四种引用
Java(七)JDK1.8新特性
Java(八)JDK1.17新特性
一、面向对象
1.1 什么是面向对象
Java是一种面向对象程序设计(OOP)的语言
。
简而言之,面向对象中,实现一个功能的载体是对象,由对象去调用对应的方法即可。而在面向过程中,某个功能的实现是由一系列函数组成的,某个函数中常常包含其他函数,由这些函数依次调用来实现复杂的功能。
对象的特点:
1.2 面向对象与面向过程的区别
类型 | 优点 | 缺点 |
---|---|---|
面向过程 | 性能比面向对象高 ,因为在面向对象中,类调用时需要实例化,开销比较大,消耗资源多。比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 |
没有面向对象易维护、易复用、易扩展 。 |
面向对象 | 易维护、易复用、易扩展 ,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。 |
性能比面向过程低 。 |
- 面向过程
一种较早的编程思想,顾名思义就是该思想是站着过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先后顺序,而每一个功能我们都使用函数(类似于方法)把这些步骤一步一步实现,使用的时候依次调用函数就即可。 - 面向对象
一种基于面向过程的新编程思想,顾名思义就是该思想是站在对象的角度思考问题,我们把多个功能合理放到不同对象里,强调的是具备某些功能的对象。
具备某种功能的实体,称为对象。面向对象最小的程序单元是:类。面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。
1.3 类中的内容
类是一组事物共有特征和功能的描述。类是对于一组事物的总体描述,是用面向对象思想进行设计时最小的单位,也是组成项目的最基本的模块。对象是类的实例,类是对象的模板
,如碳酸饮料是类,可口可乐、百事可乐、雪碧是具体的对象。
一个”.java”源文件中可以包含多个类(不是内部类),但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。
1.3.1 类中的内容
类中一般至少两种内容:成员变量和成员方法
。成员变量也叫属性,是组成的对象的数据
,比如Student对象,所具备的属性应该有姓名、所在学校、所在班级等;成员方法是操纵这些数据的行为
。
成员变量和成员方法是类中最常见的内容,但不是全部。类中所有可能的内容包括属性、方法、内部类、构造方法、代码块
。属性和方法即刚介绍的成员变量和成员方法,其他内容后续介绍。
非静态成员变量的初始化位置一般有三处:声明该变量的时候初始化、初始化块、构造方法中初始化。
静态变量的初始化位置一般有两处:声明该属性的时候初始化、静态初始化块。
1.3.2 局部变量和成员变量的区别
成员变量 | 局部变量 | |
---|---|---|
作用范围 | 作用于整个类中,其声明是在类之内、方法之外 | 作用于方法中或者语句中,其声明是在方法之内 |
在内存中的位置 | 在堆内存中 | 存在栈内存中 |
是否有默认值 | 有默认值 | 没有默认值 |
修饰符 | 属于类的,可以被public,private,static 等修饰符所修饰 | 局部变量是在方法中定义的变量或是方法的参数,不能被访问控制修饰符及 static 所修饰 |
成员变量和局部变量都能被final所修饰。
当对象通过new的方式被创建出来时,对象实体存在于堆,对象的成员变量在堆上分配空间,但对象里面的方法是没有出现的,只出现方法的声明,方法里面的局部变量并没有创建。等到对象调用此方法时,为了加快运行的速度,方法中的局部变量才会在栈中创建,所以,方法中的局部变量是在栈内的。
1.4 构造对象
在调用构造方法初始化对象时,8种基本类型的字段,会被初始化为默认值。其他类型(包括String),如果指定了某值,就被初始化为该值,否则被初始化为null。
1.4.1 构造方法
构造函数是创建对象时调用的方法,主要作用是给新创建的对象赋予一些初始变量(即完成对类对象的初始化工作)。当一个类中没有定义构造函数时,系统会默认给该类加入一个空参数的构造函数,当自己定义了构造函数后,就不再有默认的无参构造函数。
在 Java 中有多种方式可以创建对象,总结起来主要有下面的4种方式:
- 正常创建,通过new操作符。
- 反射创建,调用Class或java.lang.reflect.Constructor的newInstance()方法。
- 克隆创建,调用现有对象的clone()方法。
- 反序列化,调用java.io.ObjectInputStream的getObject()方法反序列化。
构造函数的特点:
- 函数名与类名相同;
- 无返回值,不能用void声明构造函数;
- 生成类的对象时自动执行,无需调用。
在对象较为复杂时,常常有多个构造函数,这些构造函数是以重载的形式存在的。
如果构造方法中参数过多,可以使用Builder模式。
1.4.2 构造代码块
在构造函数调用之前,还可以执行一些代码,这些代码称为构造代码块。包含构造代码块的实体类代码:
public class Thing {
/*构造代码块*/
{
System.out.println("做事前做一些准备工作");
}
public Thing(){
System.out.println("开始做事");
}
}
测试代码:
public class ThingTest {
public static void main(String[] args){
Thing thing = new Thing();
}
}
测试结果:
做事前做一些准备工作
开始做事
1.4.3 使用构造方法创建对象*
- 使用构造方法创建对象时的内存分配情况
使用构造方法创建对象,即使用new的方式创建对象。会先在堆内存中开辟一块区域,存放该对象的具体数据(即成员变量),然后在栈内存中生成一个引用,指向堆内存中生成的具体对象。如下:
需要注意的是类变量(静态变量)存在于方法区。
关于不同内存区域的特点,此处简单介绍:
内存区域 | 特点 |
---|---|
栈 | 存放局部变量、对象引用 不可以被多个线程共享 空间连续,速度快 |
堆 | 存放对象 可以被多个线程共享 空间不连续,速度慢,但是灵活 |
方法区 | 存放类的信息:代码、静态变量、字符串常量等 可以被多个线程共享 空间不连续,速度慢,但是灵活 |
- 匿名对象
在Java中,有时会创建匿名对象,匿名对象就是没有名字的对象,在创建对象时,只通过new的动作在堆内存开辟空间,却没有把堆内存空间的地址值赋值给栈内存的某个变量用以存储。
由于使用匿名对象不需要分配栈内存,且无需进行引用指向,在大量创建对象的时候能够节约很多的栈空间,且数量越多越明显。
使用匿名对象的好处是:当匿名对象使用完毕就是垃圾,垃圾回收器会在空闲时对匿名对象进行回收,节省栈内存空间
。
匿名对象的使用场景常常有两种:
1)仅仅只调用一次的时候。示例:
public class Worker {
public void say(){
System.out.println("我要当个有追求的程序员");
}
}
public class BasicTest {
public static void main(String[] args) {
new Worker().say();
}
}
1)作为参数传递。示例:
public class Book {
private String name;
public Book(String name){
this.name=name;
}
public String getName(){
return this.name;
}
public void setName(String name){
this.name=name;
}
}
public class Worker {
public void say(Book book){
System.out.println("我最近在看"+book.getName());
}
}
public class BasicTest {
public static void main(String[] args) {
Worker wooker = new Worker();
wooker.say(new Book("Java多线程编程实战指南"));
}
}
二、面向对象的特点
2.1 抽象(数据/属性和行为/方法)
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
2.2 封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但是可以提供一些可以被外界访问的方法来操作属性。
2.2.1 封装的优点
封装的优点:
- 1、隔离变化,便于使用
此处常见的是:某个类中某一功能的实现,该类自己知道就行了,即便该功能的实现方式变化了,外部调用者也仍不知道该功能实现的细节,这样就达到了“隐藏细节,隔离变化的目的”。 - 2、提高代码复用性
这个好处较容易理解,当开发者A开发了一个复杂的功能,封装到functionA中,那么开发者B就可以直接调用functionA方法,不用再重新实现该功能。 - 3、提高安全性
最常见的例子是对私有(private)属性的封装。
2.2.2 封装的例子
public class Student {
String name;
int grade;
}
public class BasicTest {
//在测试类中可以给学生修改成绩
public static void main(String[] args){
Student student = new Student();
student.grade=-10;
System.out.println("该学生的分数是:"+student.grade); //该学生的分数是:-10
}
}
该结果明显是不对的,学生的成绩不应该为负数。此时的修改常常有两个方面:将私有属性用private修饰,然后添加getter和setter方法,在setter方法中添加判断参数的逻辑。修改后的Student代码:
public class Student {
private String name;
private int grade;
public Student(){
}
public String getName(){
return this.name;
}
public void setName(String name){
if(name.length()!=0){
this.name = name;
}
}
public int getGrade(){
return this.grade;
}
public void setGrade(int grade){
if(grade > 100 || grade < 0){
System.out.println("分数参数不合法,应该是0-100的整数");
}else{
this.grade = grade;
}
}
}
修改后的测试代码:
package Basic;
public class BasicTest {
public static void main(String[] args){
Student student = new Student();
student.setGrade(-10);
//分数参数不合法,应该是0-100的整数
System.out.println("该学生的分数是:"+student.getGrade());
student.setGrade(90);
//该学生的分数是:90
System.out.println("该学生的分数是:"+student.getGrade());
}
}
从上面结果可以看出,修改后的代码可以检测非法参数,通过用调用方法而不是直接修改属性的方式来修改学生的分数,达到了提高Student类成员属性安全性的目的。
2.2.3 封装的层次(方法/类/package)
Java中的封装体有三种形式:
1、方法
最简单的封装体,方法有4种修饰符:default、private、protected和public,这4种修饰符的可见性:
2. 类
可通过访问修饰符进行隐藏。
3. 包
一般一个包(package)里是一系列相关功能模块的封装,使用别的包里的方法时,需要用import关键字。
2.2.4 访问控制级别
在定义类时,推荐访问控制级别从严处理:
1)如果不允许外部直接通过new创建对象,构造方法必须是private。
2)工具类不允许有public或default方法。
3)类非static成员变量且与子类共享,必须是protected。
4)类非static成员变量且仅在本类使用,必须是private。
5)类static成员变量仅在本类使用,必须为private。
6)若是static成员变量,必须考虑是否为final。
7)类成员方法只供类内部调用,必须是private。
8)类成员方法只对继承类公开,那么限制为protected。
2.3 继承
不同类型的对象,相互之间经常有一定数量的共同点。同时,每一个对象还定义了额外的特性使得他们与众不同。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
Java支持单继承,继承最大的优点是提高代码复用性,通常的做法是父类中定义公共的共性功能,不同的子类实现不同的差异功能。
子类是父类的扩展(extends)。
所有类的公共父类是Object。
继承是最简单,也是最常见的类与类之间的一种关系。
2.3.1 重写父类方法
当父类中的方法,并不完全适用于子类时,子类可以重写父类的方法。重写时,需要保证方法名称、参数都和父类一致。示例:
/*父类:手机*/
public class Phone {
public void playRingtone(){
System.out.println("播放手机默认铃声 ");
}
}
/*子类:荣耀手机*/
public class HonorPhone extends Phone{
@Override
public void playRingtone(){
System.out.println("播放手机铃声《荣耀》 ");
}
}
/*测试类*/
public class PhoneTest {
public static void main(String[] args){
HonorPhone honorPhone = new HonorPhone();
honorPhone.playRingtone(); //播放手机铃声《荣耀》
}
}
在重写父类的方法,需要注意以下几点:
- 子类重写父类方法,必须保证
子类方法的权限要大于或等于父类权限
,才可以重写。 - 继承当中
子类抛出的异常必须是父类抛出的异常的子异常
,或者子类抛出的异常要比父类抛出的异常要少。 - 如果返回值为引用类型,其
返回值类型必须与父类返回值类型相同或为父类返回值类型的子类
。
可以简单理解为:
权限放大,返回值和异常缩小
。
2.3.2 继承中的构造方法(子类隐式调用super())
关于子类的构造方法,在对子类对象进行初始化时,父类构造函数也会运行,是因为子类的构造函数默认第一行有一条隐式的语句super()。此处可以将上面的示例代码改下,来查看效果,示例:
/*父类:手机*/
public class Phone {
public Phone(){
System.out.println("创建手机 ");
}
public void playRingtone(){
System.out.println("播放手机默认铃声 ");
}
}
/*子类:荣耀手机*/
public class HonorPhone extends Phone{
public HonorPhone(){
System.out.println("创建荣耀手机 ");
}
public void playRingtone(){
System.out.println("播放手机铃声《荣耀》 ");
}
}
/*测试类*/
public class PhoneTest {
public static void main(String[] args){
HonorPhone honorPhone = new HonorPhone();
honorPhone.playRingtone();
}
}
测试结果:
创建手机
创建荣耀手机
播放手机铃声《荣耀》
此处需要注意的是:作为子类,无论如何都会调用父类的构造方法。默认情况下,会调用父类的无参的构造方法
。
上面的规则就会导致一个现象:当父类没有无参构造方法的时候( 提供了有参构造方法,并且不显示提供无参构造方法),子类就会抛出异常,因为它尝试去调用父类的无参构造方法
。此时,必须通过super去调用父类声明的、存在的、有参的构造方法 。示例:
public Son(){
super(123);
}
2.3.3 抽象类
在父类中定义一个方法时,可以实现一个较完整的功能,子类不重写也能完全使用。当然,父类也可以完全不实现或者部分实现某个功能,此时父类需要子类去重写这个功能,对应实现功能的方法就要用abstract关键字来标识,此时的类就叫抽象类。
抽象类的特点:
1>抽象方法一定在抽象类中。
2>抽象方法和抽象类都必须被abstract关键字修饰。
3>抽象类不可以用new创建对象。
4>抽象类中的抽象方法要想被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用,如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。
一般情况下,抽象类就是将一些父类中完全不确定或部分不确定的部分抽取出来,封装到abstract方法中,让子类去实现。当然,抽象类也可以不定义抽象方法,这样只是为了不让该类创建对象。
抽象类可以有构造方法,但是该类又不能实例化,这样做是为了什么呢?其实只是让子类调用
。
由上述内容可知抽象方法与抽象类的关系:有抽象方法存在的类一定是抽象类,抽象类中不一定有抽象方法(这样做,只是为了防止抽象类实例化)
。
2.3.4 接口
实际开发中,对于父类未实现、要子类实现方法的形式,常常接口用的更多一些,而不是抽象类。接口是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。在接口中,所有的方法都是抽象的、没有任何方法体的,其特点如下:
1>接口中一般有两种内容:常量,方法声明。
2>接口中的成员都有固定修饰符(常量:public static final;方法:public abstract)
。
接口也是不能直接通过new创建对象的,因为接口中的方法都是抽象的,而抽象方法需要被子类实现。当子类对接口中的抽象方法全都重写后,子类才可以实例化。
Java支持单继承,多实现
。这种实现方式的原因是:不能多继承,是避免不同父类中具有相同的方法,子类重写该方法时,就会引起歧义,不能确定是重写的哪个父类总的方法。而实现不会有此问题,因为不同接口中有的只是方法的声明,都没具体实现,不存在歧义。
如果不同的接口A、B有相同的接口声明,实现类实现一次即可
。
从子类和父类的角度考虑,类和接口之间的关系:
实体 | 关系 |
---|---|
类和类 | 继承 |
类和接口 | 实现 |
接口和接口 | 继承 |
抽象类可以实现接口,用来实现接口中的部分方法
。
接口与接口之间可以多继承
。接口的作用是拓展对象功能。当一个子接口继承了多个父接口时,说明子接口拓展了多个功能。当一个类实现该接口时,就拓展了多个的功能。接口多继承示例:
public interface InterfaceA {
void play();
}
public interface InterfaceB {
void study();
}
public interface InterfaceC extends InterfaceA,InterfaceB {
void work();
}
接口特点:
接口是对外暴露的规则
。要实现某个接口,就必须要重写接口里特定的方法,这个特定的方法就是接口对外的规则。接口是程序的功能扩展
。接口往往代表某种较为单一方面/模块的功能,某个类只能继承一个父类,要想实现较多维度的功能,就需要用到接口。接口的出现降低耦合度
。此处指的是Java中的向上转型,也就是父类/接口的引用可以指向子类的对象,这样在传参时就不用强制指定是哪个子类对象,传入父类/接口的引用即可,降低了耦合度。接口可以用来多实现
。一个类可以实现多个接口。- 类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口。
接口与接口直接可以有继承关系,且接口可以多继承
。
2.3.5 【抽象类和接口的对比】
抽象类在被继承时体现的是is-a关系,接口在被实现时体现的是can-do关系。与接口相比,抽象类通常是对同类事务相对具体的抽象,通常包括抽象方法、实体方法、属性变量。如果一个抽象类只有一个抽象方法,那么它就等同于一个接口。is-a关系需要符合里氏替换原则。can-do要符合接口隔离原则,实现类要有能力去实现并执行接口中定义的行为。
抽象类是模板式设计,接口是契约式设计。
在进行抽象类和接口的选择时:
is a 某种事物
的关系,可以用抽象类;has a 某种功能
的关系,可以用接口。
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
- 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
抽象类是用来捕捉子类的通用特性的,接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为(能力)的抽象,是一种行为(能力)的规范
。
- 抽象类和接口的相同点
1、接口和抽象类都不能实例化;
2、都位于继承的顶端,用于被其他实现或继承;
3、都包含抽象方法,其子类(假如抽象类的子类不是抽象类的话)都必须覆写这些抽象方法。 - 抽象类和接口的不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 |
接口方法默认修饰符是public 。并且不允许定义为 private 或者 protected(因为private 或者 protected不能修饰外部类) |
多继承 | 一个类最多只能继承一个抽象类 |
一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 |
接口的字段默认都是static和final的 |
是否可以包含静态方法 | 可以包含静态方法 |
不能包含静态方法 |
2.4 多态(父类引用指向子类对象)
多态指的是同一个行为具有多个不同表现形式或形态(调用同一个方法,不同的对象有不同的行为)的能力。扩展开来说,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。例子:
/*家庭作业接口*/
public interface Homework {
void doHomework();
}
/*两个实现类*/
public class StudentA implements Homework{
@Override
public void doHomework() {
System.out.println("同学A写了一篇作文");
}
}
public class StudentB implements Homework{
@Override
public void doHomework() {
System.out.println("同学B写了一套数学试卷");
}
}
/*测试类*/
public class StudentTest {
public static void main(String[] args){
Homework homeworkA = new StudentA();
homeworkA.doHomework(); //同学A写了一篇作文
Homework homeworkB = new StudentB();
homeworkB.doHomework(); //同学B写了一套数学试卷
}
}
从上面的例子可以看出,同一个Homework接口类型的引用,调用同样的方法,却产生了不同的结果,这就是多态最直接的体现。所以,多态最常见的方式就是:父类的引用指向了自己的子类对象。
这个例子中也能看出多态存在的三个条件:继承、重写、父类引用指向子类对象
。
多态是为了提高代码的可扩展性和维护性,方便代码透明地编写。
实现多态的两种方式:
1)使用父类作为方法形参
2)使用父类作为方法返回值
2.5 面向对象特征的相关问题
2.5.1 Java语言是如何实现多态的(父类引用指向子类对象)
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定
。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
Java实现多态有三个必要条件:
条件 | 说明 |
---|---|
继承 |
在多态中必须存在有继承关系的子类和父类 |
重写 |
子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法 |
向上转型 |
在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法 |
只有满足了上述三个条件,才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
2.5.2 在Java中定义一个空实现、无参构造方法的作用(子类会在构造方法中隐式调用父类的无参构造方法)
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”
。
因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
2.5.3在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是
帮助子类做初始化工作。
2.5.4 为什么Java不支持类多重继承
Java不支持类多重继承, 可以考虑以下两点:
- 1、钻石形继承问题产生的歧义
比如一个类A有foo()方法, 然后类B和类C派生自类A,并且有自己的foo()实现,现在类D使用多个继承派生自B和C。如果我们只引用foo(),编译器将无法决定它应该调用哪个foo(),这也称为 Diamond 问题,因为这个继承方案的结构类似于菱形。示例: - 2、多重继承确实使设计复杂化并在强制转换、构造函数链接等过程中产生问题
Java 可以通过使用接口支持多继承来避免这种歧义。由于接口只有方法声明而且没有提供任何实现,因此只有一个特定方法的实现,因此不会有任何歧义。
2.5.5 接口的字段默认都是static和final的原因
接口中如果定义非final的变量的话,而方法又都是abstract的,变量的修改就需要实现类来完成。这样一来就有悖于Sun公司开始设计interface的初衷。interface就像一个合同,规定后来的类A和B, 都按照这个合同来做事,怎么能谁想怎么改就怎么改?
另外,为什么必须为static呢?这样,未来的子类的static方法也能访问到它,可以最大限度的发挥接口属性的功能。
2.5.6 抽象类中构造方法的作用*
抽象类可以声明并定义构造函数。不过因为不可以创建抽象类的实例,所以构造函数只能通过构造函数链调用(Java中构造函数链指的是从其他构造函数调用一个构造函数),比如:抽象类的子类。也就是说,虽然抽象类自身不能实例化,但当抽象类的子类实例化时,还是会调用抽象类中的构造方法,此时抽象类构造方法初始化一些参数的作用就体现出来了
。
- 如果不能对抽象类实例化,那么构造函数的作用是什么?
它可以用来初始化抽象类内部声明的通用变量。
同时,即使抽象类没有提供任何构造函数,编译器将为抽象类添加默认的无参数的构造函数,没有的话其子类将无法编译。因为在任何构造函数中的第一条语句隐式调用super(),Java中默认超类的构造函数。
2.5.7 组合
组合就是将对象引用置于新类中即可。组合也是一种提高类的复用性的一种方式。如果你想让类有更多的扩展功能,需要记住多用组合,少用继承。
组合示例:
public class SoccerPlayer {
private String name;
private Soccer soccer;
}
public class Soccer {
private String soccerName;
}
上面代码中SoccerPlayer引用了Soccer类,通过引用Soccer类,来访问soccer中的属性和方法。组合和继承的区别:
特征 | 组合 | 继承 |
---|---|---|
关系 | 一种has-a的关系,可以理解为有一个 | 一种is-a的关系,可以理解为是一个 |
耦合性 | 组合的关系是一种松耦合的关系 | 继承的双方紧耦合 |
是否具有多态 | 不具备多态和向上转型 | 继承是多态的基础,以实现向上转型 |
时期 | 运行期绑定 | 编译期绑定 |
2.5.8 代理
除了继承和组合外,另外一种值得探讨的关系模型为代理。代理的大致描述是,A想要调用B类的方法,A不直接调用,A会在自己的类中创建一个B对象的代理,再调用B的方法。
三、重写和重载
3.1 重写(按需重新实现父类中的方法)
重写:顾名思义,就是在子类中,把父类中不满足要求的方法再重写(重写实现)一遍。在重写时,常见的形式是:子类中的方法名、参数列表、返回类值和父类中的均相同。示例:
/*父类:手机*/
public class Phone {
public void playRingtone(){
System.out.println("播放手机默认铃声 ");
}
}
/*子类:荣耀手机*/
public class HonorPhone extends Phone{
@Override
public void playRingtone(){
System.out.println("播放手机铃声《荣耀》 ");
}
}
上面的例子只是最常见的形式,在重写时,还有许多要注意的地方。重新需要注意的11条规则:
1、参数列表必须完全与被重写方法完全相同;
2、当返回值为基本数据类型时,返回值类型必须完全与被重写方法的返回类型相同;当返回值为引用类型时,重写方法的返回值类型要<=父类中返回值的类型;
3、访问权限不能比父类中被重写的方法的访问权限更低;
4、父类的成员方法只能被它的子类重写;
5、声明为final的方法不能被重写;
6、声明为static的方法不能被重写,但可以被再次声明;
7、子类和父类在同一个包中,那么子类可以重写父类中所有方法,除了声明为private和final的方法;
8、子类和父类不在同一个包中,那么子类只能重写父类中声明为public和protected的非final方法;
9、重写的方法不能抛出父类方法中不存在的异常,或者比被重写方法声明的更广泛的强制性异常;
10、构造方法不能被重写;
11、无法以返回值类型作为重载函数的区分标准。
-
1、参数列表必须完全与被重写方法完全相同
该条规则指的是重写的方法和父类中的原有方法的方法名、参数列表及参数列表的顺序完全一样才行。 -
2、当返回值为基本数据类型时,返回值类型必须完全与被重写方法的返回类型相同;当返回值为引用类型时,重写方法的返回值类型要<=父类中返回值的类型【重要】
上代码:
/*父类:手机*/
public class Phone {
String name;
public Phone getPhone(){
Phone phone = new Phone();
phone.name = "Phone";
return phone;
}
}
/*子类:荣耀手机*/
public class HonorPhone extends Phone{
@Override
public HonorPhone getPhone(){
HonorPhone honorPhone = new HonorPhone();
honorPhone.name = "HonorPhone";
return honorPhone;
}
}
/*测试类*/
public class PhoneTest {
public static void main(String[] args){
HonorPhone honorPhone = new HonorPhone();
Phone phone = new Phone();
phone = honorPhone.getPhone();
System.out.println(phone.name);
phone = phone.getPhone();
System.out.println(phone.name);
}
}
测试结果:
HonorPhone
HonorPhone
初看这个结果,挺意外的,怎么不是一个Phone、一个HonorPhone?此处就要提到一个Java中一个常用的概念向上造型
,即父类的引用指向子类的对象。看到这个,再理解这段代码,应该不就难了。测试类中前两句是创建了一个子类HonorPhone对象和父类Phone对象。从第三句开始向上造型,phone变量指向的就是子类对象,所以此时调用phone.getPhone()其实是调用的子类HonorPhone中的getPhone方法,输出结果自然也是"HonorPhone"。
回到最初的问题:为什么子类重写方法的引用类型可以是父类中方法的子类?因为这样可以更好地使用向上造型,更方便地调用子类的方法。
- 3、访问权限不能比父类中被重写的方法的访问权限更低【重要】
此处要提的是设计模式中用到的一个原则:里氏代换原则(任何基类可以出现的地方,子类一定可以出现)。如果子类的访问权限比父类小的话,在使用向上造型时,可能会出现父类引用访问不了子类接口的错误
。我们就将上面用到的例子稍微改改,将子类HonorPhone中getPhone的权限改成private,此时就能看能代码报错:
- 4、父类的成员方法只能被它的子类重写
如果两个签名相同的方法不是出现在继承关系的类中,他们也不会构成重写关系。 - 5、声明为final的方法不能被重写
- 6、声明为static的方法不能被重写,但可以被再次声明
静态方法可以被继承,但不能被重写。如果父类中定义的静态方法在子类中被重新声明,那么在父类中定义的静态方法将被隐藏,不是重写。此时子类和父类中两个静态方法并不存在任何关系。调用方法如下:
所调用的静态方法 | 调用方式 |
---|---|
父类中的静态方法 | 父类名.静态方法 |
子类中的静态方法 | 子类名.静态方法 |
- 7、子类和父类在同一个包中,那么子类可以重写父类中所有方法,除了声明为private和final的方法
- 8、子类和父类不在同一个包中,那么子类只能重写父类中声明为public和protected的非final方法
规则7和规则8可以放在一起理解,private方法和final方法不能被重写。至于访问修饰符,个人理解,能访问的才可能被重写。 - 9、重写的方法不能抛出父类方法中不存在的异常,或者比被重写方法声明的更广泛的强制性异常
- 10、构造方法不能被重写
构造方法不能被继承,所以也就不能被重写。 - 11、如果不能继承一个方法所在的父类,则不能重写这个方法
如果两个方法所以的类不存在继承关系,则谈不上重写。
3.2 重载(并存多个方法名相同的方法)
重载指的是一个类 / 子类中,方法名相同,方法参数个数 / 参数类型 / 参数顺序不同的情况。重载时需要遵守的6个规则:
- 1、方法能够在同一个类中或者在一个子类中被重载
常见的重载是出现在同一个类中的,但其实子类从父类继承一个方法,定义一个同名异参的方法也是重载。 - 2、被重载的方法,必须改变参数列表(参数个数或参数类型或参数顺序)
这个也容易理解,除了方法名,方法签名的其他位置变化都可以造成重载。 - 3、被重载的方法可以改变返回类型
这条规则可以和规则6放在一起说,返回值类型不在方法签名里,所以在重载时可以改变,但不足以作为重载的区分标准。 - 4、被重载的方法可以声明新的或更广的检查异常
重载的方法,从某种程度上,可以理解为:两个互不想干的方法,只是恰好方法名相同而已,所以异常检查范围自然可以不一样。 - 5、被重载的方法可以改变访问修饰符
修饰符不在方法签名里,所以在重载时可以改变。 - 6、无法以返回值类型作为重载函数的区分标准
3.3 重载和重写的比较
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重载
发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理
。 - 重写
发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛 出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类 方法访问修饰符为private则子类中就不是重写。
重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
。
如果父类方法访问修饰符为 private/final/static,则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 - 重载和重写的区别
重载 | 重写 | |
---|---|---|
范围 | 同一个类 | 子类中 |
参数列表 | 必须修改 | 不能修改 |
返回类型 | 可以修改 | 不可以修改 |
异常 | 可修改 | 可以减少或删除,一定不能抛出新的或更广的异常 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
出现场景 | 一个类内实现若干重载的方法,方法名相同而参数形式不同 | 在子类继承父类时,覆写(重新实现)父类中的方法 |
3.4 说出几条方法重载的最佳实践
下面有几条可以遵循的方法重载的最佳实践来避免造成自动装箱的混乱。
1)不要重载这样的方法:一个方法接收int参数,而另个方法接收Integer参数。
2)不要重载参数数量一致,而只是参数顺序不同的方法。
3)如果重载的方法参数个数多于5个,采用可变参数。
四、关键字
Java关键字的标准定义是电脑语言里事先定义的,有特别意义的标识符,有时又叫保留字。Java的关键字对Java的编译器有特殊的意义,他们用来表示一种数据类型,或者表示程序的结构等,关键字不能用作变量名、方法名、类名、包名和参数
。
4.1 所有关键字
java关键字共53个,包含两个保留字const,goto,具体:
关键字 | 意义 |
---|---|
abstract | 抽象 |
assert | 断言assert是jdk1.4引入的;jvm断言默认是关闭的;断言只适用复杂的调式过程;断言一般用于程序执行结构的判断,千万不要让断言处理业务流程 |
boolean | 布尔类型,值为true或false |
break | 结束一个语句块的执行,常用于switch…case和循环语句中 |
byte | 字节类型 |
case | 用于switch…case语句中,表示其中一个分支 |
catch | 用于try…catch语句中,用来捕捉异常 |
char | 字符类型 |
class | 类 |
const | 保留关键字 |
continue | 结束本次循环,进行下一次循环,主要用于for、while循环中 |
default | 用于switch…case语句中,表示匹配不上任何case的情况 |
do | 用于do…while循环中,其后是要默认执行一次的语句库 |
double | 双精度浮点类型 |
else | 表示条件不成立的情况 |
enum | 枚举 |
extends | 继承,用于类和类、接口和接口之间 |
final | 不可修改 |
finally | 用于try…catch语句中,用来处理不论是否捕捉到异常,都执行的语句 |
float | 单精度浮点类型 |
for | for循环,常用于有序序列的遍历 |
goto | 保留关键字 |
if | 某种条件的判定 |
implements | 实现接口,即实现接口文件中定义的空方法 |
import | 导入其他package中的类,用于跨包间的代码调用 |
instanceof | 判断某个对象是否属于某种类型 |
int | 整型数据类型 |
interface | 接口,声明一些空方法,待实现该接口的类去实现 |
long | 长整型数据类型 |
native | 代表调用本地C语言实现的接口 |
new | 创建对象 |
package | java语言中组织代码的一个单位 |
private | 私用,代表最低的访问优先级 |
protected | 被保护的,代表有继承等关联关系等的访问优先级 |
public | 公共的,代表最高访问优先级 |
return | 返回,结束方法 |
short | 短整型数据类型 |
static | 静态 |
super | 表示父类 |
switch | 多条件分支判定 |
synchronized | 同步,代表要保证线程安全 |
this | 表示当前类 |
throw | 抛出某个具体异常 |
throws | 声明在当前定义的成员方法中所有需要抛出的异常 |
transient | 表示不需要被序列化 |
try | 用于try…catch语句 |
void | 表示该方法无返回值 |
volatile | 表明两个或者多个变量必须同步地发生变化 |
while | 用于while和do…while循环语句中 |
4.2 abstract
abstract代表抽象,可以修饰类和方法。
4.2.1 抽象方法
abstract修饰方法时,代表抽象方法。抽象方法的特点:
- 抽象方法没有自己的方法体;
- 抽象方法不能用private修饰,因为抽象方法必须被子类实现(覆写),而private权限对于子类来说是不能访问的,所以就会产生矛盾;
抽象方法也不能用static修饰,如果用static修饰了,就可以直接通过类名调用,而抽象方法压根就没有任何实现,这样的调用是没有意义的
。
4.2.2 抽象类
abstract修饰类时,代表抽象类。抽象类的特点:
- 抽象类不能被实例化,也就是说我们没法直接new一个抽象类,抽象类只能由它的继承类实例化;
- 抽象类虽然不能被实例化,但有自己的构造方法;
- 抽象类与接口有很大的不同之处,接口中不能有实例方法去实现业务逻辑,而抽象类中可以有实例方法,并实现业务逻辑,比如可以在抽象类中创建和销毁一个线程池;
- 抽象类不能使用final关键字修饰,因为final修饰的类是无法被继承,而对于抽象类来说就是需要通过继承去实现抽象方法,这又会产生矛盾。
4.2.3 抽象类和抽象方法的关系
如果一个类中至少有一个抽象方法,那么这个类一定是抽象类,但反之则不然
。也就是说一个抽象类中可以没有抽象方法
。这样做的目的是为了此类不能被实例化。- 如果一个类继承了一个抽象类,那么它必须全部覆写抽象类中的抽象方法,当然也可以不全部覆写,如果不覆写全部抽象方法则这个子类也必须是抽象类。
4.3 final(不可修改/不可重写/不可继承)
final代表最终的、不可更改的,可以修饰变量、方法和类。
final修饰变量时, 如果引用为基本数据类型,则该引用为常量,该值无法修改(更准确的说法:该变量只有一次赋值的机会);如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改;如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。
final修饰方法时,表示该方法无法被子类重写,但是可以被继承。
final修饰类时,表示该类无法被继承,同时类中的所有成员方法都会被隐式定义为final方法
(只有在需要确保类中的所有方法都不被重写时才使用final修饰类)。
4.3.1 final、finally和finalize的区别
- 1、final
可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 - 2、finally
一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 - 3、finalize
是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
4.3.2 final的好处
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用final关键字,JVM会对方法、变量及类进行优化。
4.3.3 final有哪些用法
- 1、被final修饰的类不可以被继承。
- 2、被final修饰的方法不可以被重写。
- 3、被final修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。
- 4、被final修饰的方法,JVM会尝试将其内联,以提高运行效率。
- 5、被final修饰的常量,在编译阶段会存入常量池中。
除此之外,编译器对final域要遵守的两个重排序规则:
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
4.3.4 不可变类*
不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值,也就是不能改变对象的状态。Java中八个基本类型的包装类和String类都属于不可变类
,而其他的大多数类都属于可变类。不可变对象是线程安全的
。
如何设计不可变类:
1)类声明为final,不可以被继承。
2)所有成员是私有的,不允许直接被访问。
3)对变量不要setter方法。
4)所有可变的变量是final的,只能赋值一次。
5)通过构造器初始化所有成员,进行深拷贝。
6)在getter方法中不能返回对象本身,返回对象的拷贝。
示例:
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
4.4 this与super
4.4.1 this(当前对象)
this表示当前对象。
this访问本类中的属性,如果本类没有此属性则从父类中继续查找
。
this访问本类中的方法,如果本类没有此方法则从父类中继续查找
。
this调用本类构造,必须放在构造方法的首行。super调用父类构造,必须放在子类构造方法首行。
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this的用法在java中大体可以分为3种(常见的是后两种):
- 1、普通的直接引用
this相当于是指向当前对象本身。 - 2、形参与成员名字重名,用this来区分
public Person(String name, int age) {
this.name = name;
this.age = age;
}
- 3、引用本类的构造函数
class Person{
private String name;
private int age;
public Person() {
}
public Person(String name) {
this.name = name;
}
public Person(String name, int age) {
this(name);
this.age = age;
}
}
4.4.2 super(继承关系中最近的一个父类对象)
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类
。super也有三种用法:
- 1、普通的直接引用
与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 - 2、子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区
分
示例:
class Person{
protected String name;
public Person(String name) {
this.name = name;
}
}
class Student extends Person{
private String name;
public Student(String name, String name1) {
super(name);
this.name = name1;
}
public void getInfo(){
System.out.println(this.name); //Child
System.out.println(super.name); //Father
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student("Father","Child");
s1.getInfo();
}
}
- 3、引用父类构造函数
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
4.4.3 this与super的区别
this | super | |
---|---|---|
基本含义 | 代表当前对象名(如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名) | 引用当前对象的直接父类中的成员,用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名、super.成员函数据名 |
调用谁的构造方法 | 调用本类的其它构造方法 | 调用父类的构造方法 |
本质 | 指向本对象的指针 | Java关键字 |
使用this与super需要注意的地方:
- 1、super()和this()均需放在构造方法内第一行。
- 2、
this和super不能同时出现在一个构造函数里面,因为this一般会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过
。 - 3、this()和super()都指的是对象。所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
- 4、在一个构造方法中,this和super都只能调用一次。否则在实例化对象时,会因子类调用到多个父类构造方法而造成混乱。
4.5 static
static关键字主要用来修饰成员变量和成员方法,《Java编程思想》对static的描述:
static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。
简言之,使用static的目的是为了在不创建对象的前提下来调用方法/变量
。
4.5.1 静态方法、静态变量和静态代码块
- 1、静态方法
static修饰方法时,该方法称为静态方法。在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的
。 - 2、静态变量
static修饰变量时,称为静态变量。静态变量在内存中只有一个副本,它当且仅当在类初次加载时会被初始化
。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。 - 3、静态代码块
除了修饰变量和方法外,static还可以修饰代码块,称为静态代码块
。一个类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次
。
4.5.2 类的初始化顺序(先父类,后子类;先静态,后实例;先变量,后代码块,再构造函数)
具体的顺序:
- 父类静态变量
- 父类静态代码块
- 子类静态变量
- 子类静态代码块
- 父类普通变量
- 父类普通代码块
- 父类构造函数
- 子类普通变量
- 子类普通代码块
- 子类构造函数
关于该初始化顺序,可以通过如下测试代码来验证。父类测试代码:
package Basic;
public class BaseClass {
/*父类静态变量*/
public static String baseStaticVariable = "父类-->静态变量";
/*父类普通变量*/
public String baseVariable = "父类-->普通变量";
/*静态代码块*/
static{
System.out.println(baseStaticVariable);
System.out.println("父类-->静态代码块");
}
/*父类构造函数*/
public BaseClass() {
System.out.println("父类-->构造函数");
}
/*普通代码块*/
{
System.out.println(baseVariable);
System.out.println("父类-->普通代码块");
}
}
子类测试代码:
package Basic;
public class SubClass extends BaseClass{
/*子类静态变量*/
public static String subStaticVariable = "子类-->静态变量";
/*子类普通变量*/
public String subVariable = "子类-->普通变量";
public static void main(String[] args) {
new SubClass();
}
/*静态代码块*/
static {
System.out.println(subStaticVariable);
System.out.println("子类-->静态代码块");
}
/*普通代码块*/
{
System.out.println(subVariable);
System.out.println("子类-->普通代码块");
}
/*子类构造函数*/
public SubClass() {
System.out.println("子类-->构造函数");
}
}
测试结果:
父类–>静态变量
父类–>静态代码块
子类–>静态变量
子类–>静态代码块
父类–>普通变量
父类–>普通代码块
父类–>构造函数
子类–>普通变量
子类–>普通代码块
子类–>构造函数
4.5.3 static相关问题
-
1、static关键字特点(与类有关,与对象无关)
1、static是一个修饰符,用于修饰成员(成员变量,成员函数)。static修饰的成员变量称之为静态变量或类变量。
2、static修饰的成员被所有的对象共享。
3、static优先于对象存在,因为static的成员随着类的加载就已经存在。
4、static修饰的成员多了一种调用方式,可以直接被类名所调用(调用方式:类名.静态成员)。
5、static修饰的数据是共享数据,对象中的存储的是特有的数据。 -
2、成员变量和静态变量的区别
成员变量 | 静态变量 | |
---|---|---|
生命周期 | 随着对象的创建而存在随着对象的回收而释放 | 随着类的加载而存在,随着类的消失而消失 |
调用方式 | 只能被对象调用 | 可以被对象调用,也可以用类名调用(推荐用类名调用) |
别名 | 实例变量 | 类变量 |
数据存储位置 | 存储在堆内存的对象中,所以也叫对象的特有数据 | 存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据 |
- 3、静态使用时需要注意的事项
1、静态方法只能访问静态成员(非静态既可以访问静态,又可以访问非静态)。
2、静态方法中不可以使用this或者super关键字。
3、主函数是静态的。 - 4、static方法是否能被覆盖
static方法是不可以被覆盖的。因为方法覆盖是基于运行时动态绑定的,而static方法是翻译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。 - 5、static成员和static方法特点
1、生命周期和类一样
。类一加载到内存中时,static就已经加载到方法区中,随着类的结束调用而结束。优先于对象存在,static成员和static方法是先存在的,一般用来初始化一些所有对象都会用到的属性和方法;对象是后存在的,可以直接使用这些static变量和方法。
2、被所有对象所共享
。非static变量是属于某个具体的对象,而static变量是属于类的。
3、可以直接被类名调用
。 - 6、使用static的优缺点
优点:对对象的共享数据进行单独空间的存储,节省空间
。没有必要每一个对象中存储一份。
缺点:生命周期过长(可能会有垃圾不能被回收);访问出现局限性(静态虽好,但只能访问静态)。 - 7、抽象方法是否可以同时是静态的?
不能,静态方法不能重写
。 - 8、static可否用来修饰局部变量?
不能。 - 9、父类的静态方法能否被子类重写?静态属性和静态方法是否可以被继承?
父类的静态方法和属性不能被子类重写,但子类可以继承父类静态方法和属性,如父类和子类都有同名同参同返回值的静态方法show(),声明的实例Father father = new Son();
(Son extends Father),会调用father对象的静态方法。静态是指在编译时就会分配内存且一直存在,跟对象实例无关。
五、枚举
枚举是一个整型常数的集合
,用于声明一组带标识符的常数,常用于整数数量固定的场景,比如月份、星期、性别等。JDK1.5中引入了枚举,其语法为:
修饰符 enum 枚举名称:枚举变量类型
{
枚举成员
}
需要注意的是:
- 1、
枚举变量类型可以不写,默认的是int型,具体的枚举变量对应的值从0开始,逐渐递增
; - 2、
任意两个枚举成员不能具有相同的名称
,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔; - 3、引用枚举变量时的格式是:
枚举名称.枚举变量
。
总的来说,除了不能继承,基本上可以将enum看做一个常规的类。
5.1 枚举的使用*
- 用法1
public enum Color {
RED,BLUE,GREEN,BLACK;
}
public class EnumTest {
public static void main(String[] args) {
Color color = Color.BLACK;
switch(color){
case BLACK:
System.out.println("黑色");
break;
case RED:
System.out.println("红色");
break;
case GREEN:
System.out.println("绿色");
break;
case BLUE:
System.out.println("蓝色");
break;
}
}
}
测试结果为:
黑色
- 用法2
public enum EventEnum {
LAUNCH("launch"),
PAGEVIEW("pageview"),
EVENT("event");
EventEnum(String name){
this.name = name;
}
private String name;
public void show(){
System.out.println(this.name);
EventEnum[] ee = values();
for(int i = 0;i<ee.length;i++){
System.out.println(ee[i]);
}
}
}
public class Test {
public static void main(String[] args) {
EventEnum ee = EventEnum.LAUNCH;
ee.show();
String name = EventEnum.PAGEVIEW.name();
System.out.println(name);
}
}
- 用法3(声明枚举中的多个成员属性)
枚举更常见的做法是定义类似code和description(一个int型变量和一个String型变量)2个成员变量。示例:
public enum TypeEnum{
LOAN(1,"loan"),
REPAY(2,"repay");
private Integer status;
private String msg;
TypeEnum(Integer status,String msg){
this.status = status;
this.msg = msg;
}
public Integer getStatus(){
return status;
}
public String getMsg(){
return msg;
}
public static String getNameByKey(String dictValue){
for(TypeEnum enums:TypeEnum.values()){
String code = String.valueOf(enums.getStatus());
if (code.equals(dictValue))
return enums.getMsg;
}
return "";
}
}
6.2 Enum类的常用方法
枚举继承自java.lang.Enum类,所以枚举不能再继承其他类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例。Enum类的常用方法如下:
- 1、values(获取枚举成员)
该方法作用是:以数组形式返回枚举类型的所有成员,数组中元素的顺序就是在枚举中声明的顺序。
仍以上面的Color枚举为例,测试代码改为:
for(int i=0;i<Color.values().length;i++){
System.out.println(Color.values()[i]);
}
测试结果为:
RED
BLUE
GREEN
BLACK
- 2、valueOf
该方法作用是:将普通字符串转换为枚举实例。 - 3、compareTo(比较顺序)
该方法作用是:比较两个枚举成员在定义时的顺序。
将valueOf和compareTo方法联合起来使用,示例:
public static void main(String[] args) {
compare(Color.valueOf("RED"));
}
public static void compare(Color color){
for(int i=0;i<Color.values().length;i++){
System.out.println(color+"与"+Color.values()[i]+"的比较结果是:"+color.compareTo(Color.values()[i]));
}
}
测试结果为:
RED与RED的比较结果是:0
RED与BLUE的比较结果是:-1
RED与GREEN的比较结果是:-2
RED与BLACK的比较结果是:-3
- 4、ordinal(获取枚举成员的索引位置,从0开始)
该方法作用是:获取枚举成员的索引位置。
示例:
for(int i=0;i<Color.values().length;i++){
System.out.println("索引"+Color.values()[i].ordinal()
+",值:"+Color.values()[i]);
}
测试结果为:
索引0,值:RED
索引1,值:BLUE
索引2,值:GREEN
索引3,值:BLACK
5.3 枚举的实现
5.3.1 Enum类中的方法
Enum类是一个抽象类,主要有name和ordinal两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引。
Enum类中的一些方法:
//返回name
public final String name()
//返回ordinal
public final int ordinal()
//返回name
public String toString()
//直接用==比较两个对象
public final boolean equals(Object other)
//调用的是父类的 hashCode 方法
public final int hashCode()
//比较 ordinal 的大小
public final int compareTo(E o)
//根据传入的字符串name来返回对应的枚举元素
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name)
5.3.2 枚举的实现
例子:
public enum Labels0 {
ENVIRONMENT("环保"),
TRAFFIC("交通"),
PHONE("手机");
private String name;
private Labels0(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
编译后生成的字节码反编译:
枚举被编译后其实就是一个类,该类被声明成final,说明其不能被继承,因为它继承了Enum类。枚举里面的元素被声明成static final,另外生成一个静态代码块static{}
,最后还会生成values和valueOf两个方法。
更多推荐
所有评论(0)