Java基础(三)面向对象、封装继承多态、重写和重载、内部类、包装类

文章目录

  本系列文章:
    Java基础(一)基本数据类型、变量类型、修饰符、表达式、数组、分支循环、关键字
    Java基础(二)字符串、四种引用、内存泄漏、克隆、语法糖、IO
    Java基础(三)面向对象、封装继承多态、重写和重载、内部类、包装类
    Java基础(四)异常、枚举、日期、反射、泛型
    Java基础(五)Lambda表达式、Stream流、正则表达式

一、面向对象与面向过程

1.1 什么是面向对象

  Java是一种面向对象程序设计(OOP)的语言。关于面向对象与面向过程,个人的理解是,这是对解决问题方案的两种看待方式:

  • 面对对象更关注的是数据的调用方和接收方(也就是对象),解决问题变成了对象执行某一个或某一系列动作。
  • 面向过程更关于解决问题这个环节本身,是将一个个环节封装起来,也就是函数。

  简而言之,面向对象中,实现一个功能的载体是对象,由对象去调用对应的方法即可。在面向过程中,某个功能的实现是由一系列函数组成的,某个函数中常常包含其他函数,由这些函数依次调用来实现复杂的功能。
  要了解面向对象,需要先了解对象,对象的特点如下:
      

1.2 面向对象与面向过程的例子

  关于面向过程与面向对象的具体实现方式,借一个小例子来说明。比如在热门游戏《英雄联盟》中,每个英雄都有不同的皮肤,不同的实现方式:

  1. 如果用面向对象的方式来实现,就可以创建两种对象:英雄人物对象和皮肤对象,然后将两者组合在一起,就可以达到创建穿不同皮肤的不同英雄对象的目的。
  2. 如果用面向过程的方式来实现,就需要分别创建穿不同皮肤的不同英雄人物对象。

  在该例子中,面向对象实现方式的好处是"英雄"“皮肤"分离,从而提高了英雄穿戴皮肤的灵活性,从软件工程的角度考虑,就是"可维护性"比较好,因为“英雄” 和"皮肤"两个对象的耦合度比较低。软件工程追求的目标之一就是可维护性,这也是面向对象的好处之一,可维护性主要表现在3个方面:可理解性、可测试性和可修改性

1.3 面向对象与面向过程的区别

  用专业的话总结一下面向对象与面向过程的区别:

  1. 面向过程

优点:性能比面向对象高,因为在面向对象中,类调用时需要实例化,开销比较大,消耗资源多。比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。

  1. 面向对象

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:性能比面向过程低。

二、类与对象的关系

  类是一组事物共有特征和功能的描述。类是对于一组事物的总体描述,是用面向对象思想进行设计时最小的单位,也是组成项目的最基本的模块。对象是类的实例,类是对象的模板,如碳酸饮料是类,可口可乐、百事可乐、雪碧是具体的对象。
  一个”.java”源文件中可以包含多个类(不是内部类),但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。

2.1 类中的内容

2.1.1 成员变量和成员方法

  类中一般至少两种内容:成员变量和成员方法。成员变量也叫属性,是组成的对象的数据,比如Student对象,所具备的属性应该有姓名、各科目分数、所在班级、所在学校等;成员方法是操纵这些数据的行为,比如文理分科后调班、考试后取得了不同的分数等。
  关于局部变量和成员变量,简单区别如下:
1. 作用范围
  成员变量作用于整个类中,其声明是在类之内、方法之外;局部变量作用于方法中或者语句中,其声明是在方法之内。
2. 在内存中的位置
  成员变量在堆内存中,因为对象的存在,才在内存中存在;局部变量存在栈内存中。
3. 是否有默认值
  成员变量有默认值,局部变量没有默认值。
4. 修饰符
  从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

  当对象通过new的方式被创建出来时,对象实体存在于堆,对象的成员变量在堆上分配空间,但对象里面的方法是没有出现的,只出现方法的声明,方法里面的局部变量并没有创建。等到对象调用此方法时,为了加快运行的速度,方法中的局部变量才会在栈中创建,所以,方法中的局部变量是在栈内的

2.1.2 类中的全部内容

  成员变量和成员方法是类中最常见的内容,但不是全部。类中所有可能的内容包括属性、方法、内部类、构造方法、代码块。属性和方法即刚介绍的成员变量和成员方法,内部类在本文第七小节有详细介绍,构造方法也很常见,是创建对象时要执行的方法,代码块在本文中第五小节和第六小节也有介绍,指的是’{ }'符号内的代码。
  非静态成员变量的初始化位置一般有三处:
   1>声明该变量的时候初始化
   2>构造方法中初始化
   3>初始化块
  静态变量的初始化位置一般有两处:
   1>声明该属性的时候初始化
   2>静态初始化块

2.2 对象的存储

  当使用new的方式创建对象时,会先在堆内存中开辟一块区域,存放该对象的具体数据(即成员变量),然后在栈内存中生成一个引用,指向堆内存中生成的具体对象。如下:
        
  需要注意的是类变量(静态变量)存在于方法区。
  关于不同内存区域的特点,此处简单介绍,后续会有专门的文章进行分析,区别如下:

内存区域 特点
存放局部变量
不可以被多个线程共享
空间连续,速度快
存放对象
可以被多个线程共享
空间不连续,速度慢,但是灵活
方法区 存放类的信息:代码、静态变量、字符串常量等等
可以被多个线程共享
空间不连续,速度慢,但是灵活

2.3 匿名对象

  在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多线程编程实战指南"));
	}
}

三、抽象、封装、继承与多态

3.1 抽象

  抽象是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

3.2 封装

  在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。

3.2.1 封装的优点

 封装的优点:

  1. 隔离变化,便于使用
      此处常见的是:某个类中某一功能的实现,该类自己知道就行了,即便该功能的实现方式变化了,外部调用者也仍不知道该功能实现的细节,这样就达到了“隐藏细节,隔离变化的目的”。
  2. 提高代码复用性
      这个好处较容易理解,当开发者A开发了一个复杂的功能,封装到functionA中,那么开发者B就可以直接调用functionA方法,不用再重新实现该功能。
  3. 提高安全性
      此类好处,最常见的例子是对私有属性的封装。

3.2.2 常见的封装例子

  此处用一个例子来说明封装后提高安全性的作用,用Student类来表示学生,示例:

package Basic;

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示例代码:

package Basic;

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);
		System.out.println("该学生的分数是:"+student.getGrade()); //分数参数不合法,应该是0-100的整数
		student.setGrade(90);
		System.out.println("该学生的分数是:"+student.getGrade()); //该学生的分数是:90
	}	
}

  从上面结果可以看出,修改后的代码可以检测非法参数,通过用调用方法而不是直接修改属性的方式来修改学生的分数,达到了提高Student类成员属性安全性的目的。

3.2.3 封装的层次

  Java中的封装体有三种形式:
1. 函数
  最简单的封装体,此处再提一下访问修饰符,访问修饰符重要控制访问权限。不同修饰符的修饰对象,大致如下:

1>default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
2>private : 在同一类内可见。使用对象:变量、方法。
3>protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。
4>public : 对所有类可见。使用对象:类、接口、变量、方法。

修饰符 当前类 同包类 子类 其他类
public Y Y Y Y
protected Y Y Y N
default Y Y N N
private Y N N N

2. 类
  可通过访问修饰符进行隐藏。
3. 包
  一般一个包(package)里是一系列相关功能模块的封装,使用别的包里的方法时,需要用import关键字,常用系统包如下:

功能
java.lang 包含Java语言基础的类,该包系统加载时默认导入,如:System、String、Math
java.util 包含Java语言中常用工具类,如:Scanner、Random
java.io 包含输入、输出相关功能的类,如:File、InputStream

3.3 继承

  Java支持单继承,继承最大的优点是提高代码复用性,通常的做法是父类中定义公共的共性功能,不同的子类实现不同的差异功能。
  子类是父类的扩展(extends)。
  所有类的公共父类是Object。
  继承是最简单,也是最常见的类与类之间的一种关系。除此之外,还有聚合、组合和依赖等关系,设计模式相关文章中有介绍:设计模式(一)设计模式的分类与区别

3.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();  //播放手机铃声《荣耀》 
	}
}

  在重写父类的方法,需要注意以下几点:

  1. 子类重写父类方法,必须保证子类方法的权限要大于或等于父类权限,才可以重写
  2. 继承当中子类抛出的异常必须是父类抛出的异常的子异常,或者子类抛出的异常要比父类抛出的异常要少
  3. 如果返回值为引用类型,其返回值类型必须与父类返回值类型相同或为父类返回值类型的子类

可以简单理解为:权限放大,返回值和异常缩小

3.3.2 继承中的构造方法

  关于子类的构造方法,在对子类对象进行初始化时,父类构造函数也会运行,是因为子类的构造函数默认第一行有一条隐式的语句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去调用父类声明的、存在的、有参的构造方法 。

3.3.3 抽象类

  在父类中定义一个方法时,可以实现一个较完整的功能,子类不重写也能完全使用。当然,父类也可以完全不实现或者部分实现某个功能,此时父类需要子类去重写这个功能,对应实现功能的方法就要用abstract关键字来标识,此时的类就叫抽象类。
  抽象类的特点:

1>抽象方法一定在抽象类中。
2>抽象方法和抽象类都必须被abstract关键字修饰。
3>抽象类不可以用new创建对象。
4>抽象类中的抽象方法要想被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用,如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。

  一般情况下,抽象类就是将一些父类中完全不确定或部分不确定的部分抽取出来,封装到abstract方法中,让子类去实现。当然,抽象类也可以不定义抽象方法,这样只是为了不让该类创建对象。
  抽象类可以有构造方法,但是该类又不能实例化,这样做是为了什么呢?其实只是让子类调用。
  由上述内容可知抽象方法与抽象类的关系:有抽象方法存在的类一定是抽象类,抽象类中不一定有抽象方法(这样做,只是为了防止抽象类实例化)

3.3.4 接口

  实际开发中,对于父类未实现、要子类实现方法的形式,常常接口用的更多一些,而不是抽象类。接口是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。在接口中,所有的方法都是抽象的、没有任何方法体的,其特点如下:

1>接口中一般有两种内容:常量,方法声明。
2>接口中的成员都有固定修饰符(常量:public static final;方法:public abstract)

  接口也是不能直接通过new创建对象的,因为接口中的方法都是抽象的,而抽象方法需要被子类实现。当子类对接口中的抽象方法全都重写后,子类才可以实例化。
  Java支持单继承,多实现。这种实现方式的原因是:不能多继承,是避免不同父类中具有相同的方法,子类重写该方法时,就会引起歧义,不能确定是重写的哪个父类总的方法。而继承不会有此问题,因为不同接口中有的只是方法的声明,都没具体实现,不存在歧义。
  从子类和父类的角度考虑,类和接口之间的关系:

实体 关系
类和类 继承
类和接口 实现
接口和接口 继承

  抽象类可以实现接口,用来实现接口中的部分方法
  接口与接口之间可以多继承,两个父接口示例:

public interface InterfaceA {
    
    
	void play();
}
public interface InterfaceB {
    
    
	void study();
}

  继承两个父接口的子接口示例:

public interface InterfaceC extends InterfaceA,InterfaceB {
    
    
	void work();
}

  上面的代码更多的是从形式上介绍接口的特点,从总体上看的话,接口特点如下:

  1. 接口是对外暴露的规则。个人理解,此处的规则,指的是接口定义的方法声明。要实现某个接口,就必须要重写接口里特定的方法,这个特定的方法就是接口对外的规则。
  2. 接口是程序的功能扩展。接口往往代表某种较为单一方面/模块的功能,某个类只能继承一个父类,要想实现较多维度的功能,就需要用到接口。
  3. 接口的出现降低耦合度。此处指的是Java中的向上转型,也就是父类/接口的引用可以指向子类的对象,这样在传参时就不用强制指定是哪个子类对象,传入父类/接口的引用即可,降低了耦合度。
  4. 接口可以用来多实现。这个较容易理解,一个类可以实现多个接口。
  5. 类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口
  6. 接口与接口直接可以有继承关系,且接口可以多继承

3.3.6 抽象类和接口的对比

  抽象类是用来捕捉子类的通用特性的,接口是抽象方法的集合。
  从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为(能力)的抽象,是一种行为(能力)的规范
  抽象类和接口的相同点:
   1、接口和抽象类都不能实例化
   2、都位于继承的顶端,用于被其他实现或继承
   3、都包含抽象方法,其子类(假如抽象类的子类不是抽象类的话)都必须覆写这些抽象方法
  抽象类和接口的不同点:

参数 抽象类 接口
声明 抽象类使用abstract关键字声明 接口使用interface关键字声明
实现 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现
构造器 抽象类可以有构造器 接口不能有构造器
访问修饰符 抽象类中的方法可以是任意访问修饰符 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected(因为private 或者 protected不能修饰外部类)
多继承 一个类最多只能继承一个抽象类 一个类可以实现多个接口
字段声明 抽象类的字段声明可以是任意的 接口的字段默认都是 static 和 final 的

  在进行抽象类和接口的选择时,有一种角度是:is a 某种事物的关系,可以用抽象类;has a 某种功能的关系,可以用接口。
  接口的字段默认都是 static 和 final 的原因:接口中如果定义非final的变量的话,而方法又都是abstract的,变量的修改就需要实现类来完成。这样一来就有悖于Sun公司开始设计interface的初衷。interface就像一个合同,规定后来的类A和B, 都按照这个合同来做事,怎么能谁想怎么改就怎么改?另外,为什么必须为static呢?这样,未来的子类的static方法也能访问到它,可以最大限度的发挥接口属性的功能。
  Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。所以,在JDK1.8之后,就可以为接口提供默认实现的方法了,并且不用强制子类来实现它。
  接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
   1、行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
   2、选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。

3.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)使用父类作为方法返回值

3.5 什么是多态机制?Java语言是如何实现多态的?【重要】

  所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
  多态分为编译时多态运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性
  Java实现多态有三个必要条件:
   1、继承:在多态中必须存在有继承关系的子类和父类。
   2、重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
   3、向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

  只有满足了上述三个条件,才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
  对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

四、重写和重载

4.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、当返回值为基本数据类型时,返回值类型必须完全与被重写方法的返回类型相同;当返回值为引用类型时,重写方法的返回值类型要<=父类中返回值的类型【重要】
      此处参考网上的一篇文章来解释这个现象,先上代码:
/*父类:手机*/
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的方法不能被重写
      这句话也容易理解,这是final关键字的性质。
  • 6、声明为static的方法不能被重写,但可以被再次声明
      静态方法可以被继承,但不能被重写。如果父类中定义的静态方法在子类中被重新声明,那么在父类中定义的静态方法将被隐藏,不是重写。此时子类和父类中两个静态方法并不存在任何关系。调用方法如下:
所调用的静态方法 调用方式
父类中的静态方法 父类名.静态方法
子类中的静态方法 子类名.静态方法
  • 7、子类和父类在同一个包中,那么子类可以重写父类中所有方法,除了声明为private和final的方法
  • 8、子类和父类不在同一个包中,那么子类只能重写父类中声明为public和protected的非final方法
      规则7和规则8可以放在一起理解,private方法和final方法不能被重写。至于访问修饰符,个人理解,能访问的才可能被重写。
  • 9、重写的方法不能抛出父类方法中不存在的异常,或者比被重写方法声明的更广泛的强制性异常
  • 10、构造方法不能被重写
      构造方法不能被继承,所以也就不能被重写。
  • 11、如果不能继承一个方法所在的父类,则不能重写这个方法
      规则11和规则4可以放在一起理解,如果两个方法所以的类不存在继承关系,则谈不上重写。

4.2 重载

  重载指的是一个类/子类中,方法名相同,方法参数个数 / 参数类型 / 参数顺序不同的情况。重载时需要遵守的6个规则如下:

  • 1、方法能够在同一个类中或者在一个子类中被重载
      常见的重载是出现在同一个类中的,但其实子类从父类继承一个方法,定义一个同名异参的方法也是重载。
  • 2、被重载的方法,必须改变参数列表(参数个数或参数类型或参数顺序)
      这个也容易理解,除了方法名,方法签名的其他位置变化都可以造成重载。
  • 3、被重载的方法可以改变返回类型
      这条规则可以和规则6放在一起说,返回值类型不在方法签名里,所以在重载时可以改变,但不足以作为重载的区分标准。
  • 4、被重载的方法可以声明新的或更广的检查异常
      重载的方法,从某种程度上,可以理解为:两个互不想干的方法,只是恰好方法名相同而已,所以异常检查范围自然可以不一样。
  • 5、被重载的方法可以改变访问修饰符
      修饰符不在方法签名里,所以在重载时可以改变。
  • 6、无法以返回值类型作为重载函数的区分标准

五、构造方法

5.1 构造方法使用

  构造函数是创建对象时调用的方法,主要作用是给新创建的对象赋予一些初始变量。当一个类中没有定义构造函数时,系统会默认给该类加入一个空参数的构造函数,当自己定义了构造函数后,就不再有默认的无参构造函数。
  此处简单提一句,在 Java 中有多种方式可以创建对象,总结起来主要有下面的 4 种方式:

  1. 正常创建,通过 new 操作符。
  2. 反射创建,调用 Class 或 java.lang.reflect.Constructor 的 newInstance()方法。
  3. 克隆创建,调用现有对象的 clone()方法。
  4. 反序列化,调用 java.io.ObjectInputStream 的 getObject()方法反序列化。

  构造函数的特点:

1>函数名与类名相同
2>无返回值
3>无return语句

  在对象较为复杂时,常常有多个构造函数,这些构造函数是以重载的形式存在的。

5.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();	
	}
}

  测试结果:

做事前做一些准备工作
开始做事

六、关键字

6.1 this关键字

  this关键字代表当前对象,用在构造函数期间,使用方式有两种:

  1. 给成员变量赋值,这种使用方式十分常见。示例:
public class Person {
    
    
	String name;
	int age;
	
	public Person(String name,int age){
    
    
		this.name=name;
		this.age=age;
	}
}
  1. 构造函数之间的互相调用,这种方式在原生的复杂对象,如String、File等对象中较为常用,以为他们的构造函数都不止一种。
      this调用构造方法时,要位于第一行。示例:
public class Person {
    
    
	String name;
	int age;
	
	public Person(){
    
    
		System.out.println("开始创建Person对象"); 
	}
	
	public Person(String name,int age){
    
    
		this();
		this.name=name;
		this.age=age;
	}
}

6.2 static关键字

  static关键字可以修饰成员变量和成员方法,静态的意思是不跟随对象,是和类共存亡的。static成员和static方法特点如下:

  1. 生命周期和类一样
      类一加载到内存中时,static就已经加载到方法区中,随着类的结束调用而结束。优先于对象存在,static成员和static方法是先存在的,一般用来初始化一些所有对象都会用到的属性和方法;对象是后存在的,可以直接使用这些static变量和方法。
  2. 被所有对象所共享
      非static变量是属于某个具体的对象,而static变量是属于类的。
  3. 可以直接被类名调用
      static变量一般是通过类名来调用的,虽然也可以被对象调用。示例:
/*实体类:Person*/
public class Person {
    
    
	String name;
	int age;
	static String status="alive";
	
	public Person(){
    
    
		System.out.println("开始创建Person对象"); 
	}
	
	public Person(String name,int age){
    
    
		this();
		this.name=name;
		this.age=age;
	}
}
/*测试类*/
public class BasicTest {
    
    
	public static void main(String[] args){
    
    
		Person person = new Person("John",73);
		System.out.println("Preson的状态是:"+Person.status);
		System.out.println(person.name+"的状态是:"+Person.status);
		
	}	
}

  测试结果:

开始创建Person对象
Preson的状态是:alive
John的状态是:alive

  在使用静态方法时,需要注意:

  1. 静态方法只能访问静态成员,因为静态方法是优先对象存在的。当加载静态方法时,对象可能还未创建,所以不能访问属于对象的非静态成员。
  2. 静态方法中不可以写this,super关键字,道理同上。

  静态变量存在的意义是:节省各个对象共性变量的存储空间,所有对象公用一个,而不是有多少对象就有多少这样的变量。

  一般工具类的方法用static修饰。

  实例变量和静态变量的区别:

变量类型 存储位置 生命周期
静态变量 方法区 随着类的消失而消失
实例变量 堆内存 随着对象的消失而消失

  静态方法只能访问静态成员(成员变量和方法),非静态方法既可以访问静态成员也可以访问非静态成员。
  static使用的好处是:对对象的共享数据进行单独空间的存储,节省空间。没有必要每一个对象中存储一份。
  static使用的坏处是:生命周期过长(可能会有垃圾不能被回收);访问出现局限性(静态虽好,但只能访问静态)。
  前面介绍过构造代码块,如果在代码块前面加一个static关键字,就成了静态代码块。静态代码块随着类的加载而执行,且执行一次,用于给类进行初始化,且优先于主函数、代码块、构造方法执行。这些方法的执行顺序,可以通过一个例子来看。示例:

/*实体类:Person*/
public class Person {
    
    
	String name;
	int age;
	static String status="alive";
	
	static{
    
    
		System.out.println("实体类-->静态代码块"); 
	}
	{
    
    
		System.out.println("实体类-->代码块"); 
	}
	
	public Person(){
    
    
		System.out.println("开始创建Person对象"); 
	}
	
	public Person(String name,int age){
    
    
		this();
		this.name=name;
		this.age=age;
	}
}
/*测试类*/
public class PersonTest {
    
    
	static{
    
    
		System.out.println("测试类-->静态代码块"); 
	}
	public static void main(String[] args){
    
    
		Person person = new Person("John",73);
		System.out.println(person.name+"的状态是:"+Person.status);
	}
}

  测试结果:

测试类–>静态代码块
实体类–>静态代码块
实体类–>代码块
开始创建Person对象
John的状态是:alive

6.3 static存在的主要意义

  static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
  static关键字还有一个比较关键的作用就是:·用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次·。
  为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

6.4 static的独特之处

  1. 被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
  2. 在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
  3. static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的。
  4. 被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。

6.5 static应用场景

  因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。
  因此比较常见的static应用场景有:

1、修饰成员变量
2、修饰成员方法
3、静态代码块
4、修饰类(只能修饰内部类也就是静态内部类)。

6.6 final关键字

  final也是在继承关系中,一个较为常用的关键字。final修饰变量时,变量不能被修改;final修饰方法时,方法不能被重写;final修饰类时,类不能被继承。

6.7 final finally finalize区别

  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize()回收垃圾,子类可以覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

  三者更详细的区别可以参考如下文章:final finally finalize 区别及用法

七、内部类

7.1 内部类的定义

  将一个类的定义放在里另一个类的内部,就是内部类
  先看一个内部类的简单例子,包含内部类的类示例:

public class OutClass {
    
    
    private String outStr ="外部类-->字符串";
    class InnerClass{
    
    
        private String inStr= "内部类中的字符串";
        public void print(){
    
    
            //调用外部类的outStr属性
            System.out.println(outStr);
        }
    }
    
    //在外部类中定义一个方法,该方法负责产生内部类对象并调用print()方法
    public void printString(){
    
    
        //内部类对象
    	InnerClass in = new InnerClass();
        //内部类对象提供的print
        in.print();
    }
}

  测试类代码:

public class InnerTest {
    
    
    public static void main(String[] args){
    
    
        OutClass out = new OutClass();
        out.printString();  //外部类-->字符串
    }
}

7.2 内部类的优点

  1. 内部类可以实现和外部类不同的接口,也可以继承和外部类不同的类,间接完成功能扩展
  2. 内部类中的属性、方法可以和外部类重名,但并不冲突,因为内部类是具有类的基本特征的独立实体。
  3. 内部类利用访问修饰符隐藏内部类的实施细节,提供了更好的封装,除外部类,都不能访问。
  4. 静态内部类使用时可直接使用,不需先创造外部类。
  5. 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据
  6. 内部类有效实现了“多重继承”,即用内部类去继承其他类,优化 java 单继承的缺陷;
  7. 匿名内部类可以很方便的定义回调
      匿名内部类往往是做为一个内部类(接口)的具体实现。在一些平台类(platform class)中有一些模板方法。模板方法的包含了固定流程。其中某些步骤是调用了内部类(接口)中的某些方法。但是平台类将这些方法的具体实现延迟到了业务类中。业务类调用平台类的模板方法,但是传入匿名内部类的实现做为模板方法的参数。

7.3 内部类的分类

  内部类可以分为四种:成员内部类、静态内部类、局部内部类、匿名内部类

7.3.1 成员内部类

  成员内部类是最常见的内部类,即一个类中嵌套了另一个类,无特殊修饰符。成员内部类的语法:

new 外部类().new 内部类()

  先看一个例子,来了解成员内部类的使用,包含内部类的类,示例:

package Inner;

public class OutClass {
    
    
    private int outerVariable = 1;
    private int commonVariable = 2;
    private static int outerStaticVariable = 3;
    
    public class Inner {
    
    
        
        private int commonVariable = 20;
        public Inner() {
    
    
        }

        public void innerShow() {
    
    
            /*当和外部类属性名相同时,直接引用属性名,访问的是内部类的成员属性*/
            System.out.println("内部类、外部类中变量同名时,直接访问的是内部的变量:" + commonVariable);
            /*不同名情况下,内部类可直接访问外部属性*/
            System.out.println("outerVariable:" + outerVariable+",outerStaticVariable:"+outerStaticVariable);
            /*当和外部类属性名相同时,可通过外部类名.this.属性名来访问外部变量*/
            System.out.println("内部类、外部类中变量同名时,需要用外部类类名来访问外部的变量:" + OutClass.this.commonVariable);
        }
    }
    
    /*将内部类中的接口,包装成外部类中的方法,这样其他类可方便地调用内部类中的接口*/
    public void outerShow() {
    
    
        Inner inner = new Inner();
        inner.innerShow();
    }
}

  测试代码:

public class InnerTest {
    
    
	public static void main(String[] args){
    
    
		OutClass outClass=new OutClass();
		outClass.outerShow();
	}
}

  这个例子中可以看出内部类和外部类访问的一些简单规则:内部类可以直接访问外部类中的(包括private)变量,外部类访问内部类变量时,则需要先创建内部类对象。当内部类和外部类中有相同名称的变量时,在内部类中需要用“外部类.this.变量名”的形式才能访问。
  当然,在其他类中,也可以创建内部类对象,调用内部类中的方法,修改测试代码即可:

public class InnerTest {
    
    
	public static void main(String[] args){
    
    		
		OutClass outer = new OutClass();
		OutClass.Inner inner = outer.new Inner();
	    inner.innerShow();
	}
}

  上面测试代码的输出结果与之前测试结果一致,并且这也是创建内部类对象的固定格式,即:

先用new的方式,创建外部类对象,如OutClass outer = new OutClass()
然后用 “外部类类名.内部类类名 内部类变量名 = 外部类对象.new 内部类类名()” 的方式创建内部类对象

 成员内部类的特点:

  1. 可以是任何的访问修饰符。
  2. 成员内部类的内部不能有静态信息
  3. 成员内部类也是类,具有普通类的特性,如继承、重写、重载等。
  4. 外部类要访问内部类信息,需要先创建内部类对象,才能访问
  5. 成员内部类可以直接使用外部类的任何信息,如果属性或者方法同名,调用外部类.this.属性或者方法

7.3.2 静态内部类

  顾名思义,是用static修饰的内部类。在静态内部类中,只能访问外部类中static方法和static变量,其他用法与成员内部类相似。静态内部类实例创建的语法:

new 外部类.静态内部类()

  有意思的在于静态代码块的调用,先看例子:

/*外部类*/
public class OutClass {
    
    
    private int outerVariable = 1;
    private int commonVariable = 2;
    
    private static int outerStaticVariable = 3;

    static {
    
    
        System.out.println("OutClass-->静态块");
    }

    public static void outerStaticMethod() {
    
    
        System.out.println("外部类-->静态方法");
    }

    public static class Inner {
    
    
        private int innerVariable = 10;
        private int commonVariable = 20;

        static {
    
    
            System.out.println("Inner-->静态块");
        }

        private static int innerStaticVariable = 30;

        public void innerShow() {
    
    
            System.out.println("内部类中变量innerVariable:" + innerVariable);
            System.out.println("内部类中与外部类同名变量commonVariable:" + commonVariable);
            System.out.println("外部类中变量outerStaticVariable:"+outerStaticVariable);
            outerStaticMethod();
        }

        public static void innerStaticShow() {
    
    
        	//被调用时会先加载Outer类
            //outerStaticMethod();
        }
    }

    public static void callInner() {
    
    
        System.out.println(Inner.innerStaticVariable);
        Inner.innerStaticShow();
    }
}
/*测试类*/
public class InnerTest {
    
    
    public static void main(String[] args) {
    
    
        //访问静态内部类的静态方法,Inner类被加载,此时外部类未被加载,独立存在,不依赖于外围类。
    	OutClass.Inner.innerStaticShow();
        //访问静态内部类的成员方法
//    	OutClass.Inner oi = new OutClass.Inner();
//        oi.innerShow();
    }
}

  此时的测试结果为:

Inner–>静态块

  从这个例子可以看出,当不访问外部类中的static变量或static方法时,是不会调用外部类的静态代码块的。将上述外部类代码稍微修改:

public class OutClass {
    
    
    private int outerVariable = 1;
    private int commonVariable = 2;
    
    private static int outerStaticVariable = 3;

    static {
    
    
        System.out.println("OutClass-->静态块");
    }

    public static void outerStaticMethod() {
    
    
        System.out.println("外部类-->静态方法");
    }

    public static class Inner {
    
    
        private int innerVariable = 10;
        private int commonVariable = 20;

        static {
    
    
            System.out.println("Inner-->静态块");
        }

        private static int innerStaticVariable = 30;

        public void innerShow() {
    
    
            System.out.println("内部类中变量innerVariable:" + innerVariable);
            System.out.println("内部类中与外部类同名变量commonVariable:" + commonVariable);
            System.out.println("外部类中变量outerStaticVariable:"+outerStaticVariable);
            outerStaticMethod();
        }

        public static void innerStaticShow() {
    
    
        	//被调用时会先加载OutClass类
            outerStaticMethod();
        }
    }

    public static void callInner() {
    
    
        System.out.println(Inner.innerStaticVariable);
        Inner.innerStaticShow();
    }
}

  此时的测试结果:

Inner–>静态块
OutClass–>静态块
外部类–>静态方法

  可以看出,在内部类中访问外部static变量或static方法时,就会加载外部类的静态代码块,不过是在加载完内部类的静态代码块之后
  静态内部类的特点:

1>静态内部类的方法只能访问外部类的static变量或static方法。
2>访问内部类的静态信息的形式是:直接外部类.内部类.静态信息。
3>静态内部类可以独立存在,不依赖于其他外部类。

7.3.3 局部内部类(了解即可)

  局部内部类的位置和之前的两个类不一样,不再是在一个类内部,而是在方法内部。局部内部类的使用,和之前的两种内部类差别主要有两点:

  1. 访问方法内的变量时,变量需要用final修饰
  2. 局部内部类只能在方法内使用

  示例代码如下:

package Inner;
/*外部类*/
public class OutClass {
    
    
    private int outerVariable = 1;
    private int commonVariable = 2;
    private static int outerStaticVariable = 3;

    public void outerMethod() {
    
    
        System.out.println("外部类-->普通方法");
    }

    public static void outerStaticMethod() {
    
    
        System.out.println("外部类-->静态方法");
    }
    
    public void outerCreatMethod(final int value) {
    
    
        final boolean inOut = false;

        class Inner {
    
    

            private int innerVariable = 10;
            private int commonVariable = 20;

            public void innerShow() {
    
    
                System.out.println("内部类-->变量innerVariable:" + innerVariable);
                /*局部变量*/
                System.out.println("是否直接在外部类中:" + inOut);
                System.out.println("内部类所在方法的参数value:" + value);
                /*访问外部类的变量、方法*/
                System.out.println("外部类中的普通变量outerVariable:" + outerVariable);
                System.out.println("访问内部类的同名变量commonVariable:" + commonVariable);
                System.out.println("访问外部类的同名变量commonVariable:" + OutClass.this.commonVariable);
                System.out.println("外部类中的静态变量outerStaticVariable:" + outerStaticVariable);
                outerMethod();
                outerStaticMethod();
            }
        }
        /*局部内部类只能在方法内使用*/
        Inner inner = new Inner();
        inner.innerShow();
    }
}

  测试类代码如下:

public class InnerTest {
    
    
    public static void main(String[] args) {
    
    
    	OutClass outer = new OutClass();
        outer.outerCreatMethod(100);
    }
}

  测试结果如下:

内部类–>变量innerVariable:10
是否直接在外部类中:false
内部类所在方法的参数value:100
外部类中的普通变量outerVariable:1
访问内部类的同名变量commonVariable:20
访问外部类的同名变量commonVariable:2
外部类中的静态变量outerStaticVariable:3
外部类–>普通方法
外部类–>静态方法

  局部内部类的特点:

1>类前不能有访问修饰符。
2>使用范围为当前方法内。
3>不能声明static变量和static方法。
4>JDK8以前(不包括8)只能访问被final修饰的变量,不论是方法接收的参数,还是方法内的参数,JDK8后隐式地加上final
5>可以随意的访问外部类的变量和方法。

7.3.4 匿名内部类

  匿名内部类也是一种常见的内部类,其实匿名内部类本质上是一个重写或实现了父类或接口的子类对象。使用场景:一般是只使用一次某个接口的实现类时
  上例子:

/*定义一个接口*/
public interface Sport{
    
    
	void play();
}
/*测试类*/
public class OutClass {
    
    
	
    public static void main(String[] args){
    
    
    	OutClass.getInnerInstance("打篮球").play();
    }
	
    public static Sport getInnerInstance(final String sport){
    
    
        return new Sport(){
    
    
            @Override
            public void play(){
    
    
                System.out.println(sport);
            }
        };
    }
}

  测试结果:

打篮球

  匿名内部类的特点:

1>匿名内部类无访问修饰符。
2>使用匿名内部类的主要目的重写new后的类的某个或某些方法(匿名内部类必须继承一个抽象类或者实现一个接口)。
3>匿名内部类访问方法参数时也有和局部内部类同样的限制。
4>匿名内部类没有构造方法。
5>当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
6>匿名内部类不能定义任何静态成员和静态方法。

  在Android开发中,常用匿名内部类来实现事件监听和回调。当然最常见的匿名内部类例子:

        new Thread( new Runnable() {
    
      
            public void run(){
    
                             
                System.out.println("test");
            }
        }).start(); 

7.4 内部类有哪些应用场景

  • 1、一些多算法场合
      将部分实现转移给使用者,让使用者决定算法的实现。
  • 2、解决一些非面向对象的语句块
      也是类似于模板方法模式的使用,将if…else if…else语句,case语句等转移到子类中去实现。
  • 3、适当使用内部类,使得代码更加灵活和富有扩展性
      此处常见的是使用模板方法模式的时候,将部分接口的实现转移到子类中实现
  • 4、当某个类除了它的外部类,不再被其他的类使用时
      一个内部类依附于它的外部类而存在,可能的原因有:
       1>不可能为其他的类使用;
       2>出于某种原因,不能被其他类引用,可能会引起错误。
      这个场景是我们使用内部类比较多的一个场景。

7.5 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

  先说结论:在JDK8之前,如果我们在匿名内部类中需要访问局部变量,那么这个局部变量必须用final修饰符修饰;在JDK8中如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰,其原因是:看似是一种编译机制的改变,实际上就是一个语法糖(底层还是帮你加了final)。
  这种现象的原因是:用final修饰实际上就是为了保护数据的一致性。这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。
为什么需要用final保护数据的一致性呢?
  因为将数据拷贝完成后,如果不用final修饰,则原先的局部变量可以发生变化。这里到了问题的核心了,如果局部变量发生变化后,匿名内部类是不知道的(因为他只是拷贝了局部变量的值,并不是直接使用的局部变量)。这里举个栗子:原先局部变量指向的是对象A,在创建匿名内部类后,匿名内部类中的成员变量也指向A对象。但过了一段时间局部变量的值指向另外一个B对象,但此时匿名内部类中还是指向原先的A对象。那么程序再接着运行下去,可能就会导致程序运行的结果与预期不同。

八、包装类

8.1 包装类由来

  Java是一个面向对象的语言,同时Java中存在着8种基本数据类型,为每个基本数据类型设计一个对应的类进行代表,这种方式增强了Java面向对象的性质。
  很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,无法将int 、double等类型放进去的,因为集合的容器要求元素是Object类型。而包装类型的存在使得向集合中传入数值成为可能,包装类的存在弥补了基本数据类型的不足
  此外,包装类还为基本类型添加了属性和方法,丰富了基本类型的操作。比如int类型的最大值和最小值,直接用哪个Integer.MAX_VALUE和Integer.MIN_VALUE表示即可。
  Java有8种基本数据类型:byte、short、int、long、float、double、boolean、char,因此包装类也有8种:

基本类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Character
char Boolean

8.2 自动装箱/自动拆箱机制

  因为包装类是对象,而基本数据不是对象,所以预想中,两者应该是有转换机制的,示例:

		/*基本数据类型转为包装类*/
		Integer num1 = new Integer(1);	
		/*包装类型转为基本数据类型*/
		int num2 = num1.intValue();		
		System.out.println("包装类值:"+num1+",基本类型值:"+num2);

  所谓的自动装箱和自动拆箱,就是说不用这么明显的转换,系统会默认装换,示例代码如下:

		/*自动装箱*/
		Integer num1 = 1;	
		/*自动拆箱*/
		int num2 = num1;		
		System.out.println("包装类值:"+num1+",基本类型值:"+num2);  //包装类值:1,基本类型值:1

  基本类型和包装类型为什么可以直接相互赋值呢?这其实是Java中的一种“语法糖”。“语法糖”是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
  在自动装箱的时候,基本类型要与包装类类型一一对应;自动拆箱的时候,包装类类型 <= 基本类型就可以

8.3 包装类和基本类型的互相装换

  其实在上一小节已经写了两者互相转换的规则,简单列一下:

基本类型 基本类型–>包装类 包装类–>基本类型
byte new Byte / valueOf byteValue
short new Short / valueOf shortValue
int new Integer / valueOf intValue
long new Long / valueOf longValue
float new Float / valueOf floatValue
double new Double / valueOf doubleValue
boolean new Boolean / valueOf booleanValue
char new Character / valueOf charValue

  以Integer为例,我们看一下它的valueOf 方法,示例如下:

    public static Integer valueOf(int i) {
    
    
        return  i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128];
    }

  从上面代码可以看出,其实包装类的valueOf方法,还是通过 ‘new 包装类()’ 的方式来创建包装类对象的。
  基本类型和包装类互相转换,示例代码如下:

		/*基本型转换为包装类对象*/
		Byte num1 = new Byte((byte) 1);
		Short num2 = new Short((short) 2); 
		Integer num3 = new Integer(3);	
		Long num4 = new Long(4);
		Float num5 = new Float(5.0);
		Double num6 = new Double(6.0);
		Character num7 = new Character((char) 99);
		Boolean bool1 = new Boolean(true);
		System.out.println("包装类值,Byte型:"+num1+",Short型:"+num2+",Integer型:"+num3+",Long型:"+num4
				+",Float型:"+num5+",Double型:"+num6+",Character型:"+num7+",Boolean型:"+bool1);
		
		/*包装类转换为基本类型*/
		byte num11 = num1.byteValue();
		short num12 = num2.shortValue();
		int num13 = num3.intValue();
		long num14 = num4.longValue();
		float num15 = num5.floatValue();
		double num16 = num6.doubleValue();
		char num17 = num7.charValue();
		boolean bool2 = bool1.booleanValue();
			
		System.out.println("基本类型值,byte型:"+num11+",short型:"+num12+",int型:"+num13+",long型:"+num14
				+",float型:"+num15+",double型:"+num16+",char型:"+num17+",boolean型:"+bool2);	

  测试结果:

包装类值,Byte型:1,Short型:2,Integer型:3,Long型:4,Float型:5.0,Double型:6.0,Character型:c,Boolean型:true
基本类型值,byte型:1,short型:2,int型:3,long型:4,float型:5.0,double型:6.0,char型:c,boolean型:true

8.4 注意事项

1. 空指针问题
 常见的形式是:

		Integer num1 = null;
		int num2 = num1;

   此时运行代码,会提示空指针,原因是:将num1的值赋给num2时,会先进行自动拆箱,也就是num1.intValue(),此时num1是null,所以报了空指针异常。
2. ==判断相等问题
   == 对于基本变量,比较的是变量的值。==对于引用变量,比较的是对象的地址,也就是是否是同一个对象。先看个例子:

		Integer int1 = 1;
		Integer int2 = 1;
		System.out.println(int1 == int2);  //true

		Integer int3 = 200;
		Integer int4 = 200;
		System.out.println(int3 == int4);  //false

   用int值创建Integer对象时,有个默认装箱的操作,所以该问题的关键就变成了什么时候会创建相同的Integer对象?这个问题需要在源码中寻找:

public static Integer valueOf(int i) {
    
    
    // 判断实参是否在可缓存范围内,默认为[-128, 127]
    if (i >= IntegerCache.low && i <= IntegerCache.high) 
        return IntegerCache.cache[i + (-IntegerCache.low)]; 
    return new Integer(i); 
}

   从这段源码可以看出,当用[-128, 127]范围内的值作为参数创建Integer对象时,会创建相同的对象;否则会创建出不同的对象。

九、对象实例化的四种方式

9.1 通过new关键字创建对象

  该方式较为常见,不再赘述。

9.2 通过clone方法创建对象

  clone()方法可以用来创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,而互不影响,就需要用到java中对象的复制。利用clone创建对象,示例:

/*实体类*/
public class Person implements Cloneable{
    
    
	String name;
	int age;
	
	public Person(){
    
    
		
	}
	
	public Person(String name,int age) {
    
    
		this.name = name;
	    this.age = age;
	}

	public String getName() {
    
    
	    return name;
	}

	public int getAge() {
    
    
	    return age;
	}

	public void setName(String name) {
    
    
	    this.name = name;
	}

	public void setAge(int age) {
    
    
	    this.age = age;
	}

	@Override
	protected Object clone() throws CloneNotSupportedException {
    
    
	    return super.clone();
	}

	@Override
	public String toString() {
    
    
	    String s = "姓名是:"+this.name+",年龄是:"+this.age;
	    return s;
	 }
}
/*测试类*/
public class ObjectTest {
    
    
	public static void main(String[] args) throws CloneNotSupportedException  {
    
    
		//new关键字创建的对象
		Person p1 = new Person("李元芳", 30);  
        System.out.println(p1);  //姓名是:李元芳,年龄是:30
        Person p2 = null;
        
        //调用对象的clone()方法创建对象
        p2 = (Person) p1.clone();
        System.out.println(p2);  //姓名是:李元芳,年龄是:30
        p2.setAge(51);
        p2.setName("狄仁杰");
        System.out.println(p2);  //姓名是:狄仁杰,年龄是:51
	}
}

9.3 通过反射创建对象

  简单来说,JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
  将上面代码进行简单修改,利用反射创建对象,示例:

/*实体类*/
public class Person {
    
    
	String name;
	int age;
	
	public Person(){
    
    
		System.out.println("无参构造方法");
	}
	
	public Person(String name){
    
    
		this.name = name;
		System.out.println("一个参数构造方法");
	}
	
	public Person(String name,Integer age) {
    
    
		this.name = name;
	    this.age = age;
	    System.out.println("两个参数构造方法");
	}


	@Override
	public String toString() {
    
    
	    String s = "姓名是:"+this.name+",年龄是:"+this.age;
	    return s;
	 }
}
/*测试类*/
public class ObjectTest {
    
    
	public static void main(String[] args) {
    
    
		try{
    
    
			Class clazz = Class.forName("Basic.Person");
			/**
		     * 调用无参构造函数,
		     * 效果上和使用class.newInstance()差不多
		     * 构造函数无参的情况下,可以传入一个空数组,也可以不传入数组
		     */
		    Constructor<Person> con = clazz.getConstructor();
		    Person thisIsTestClass = con.newInstance();
		        
		    //依然是无参构造函数
		    Class[] paramTypeEmpty = {
    
    };
		    Constructor<Person> con0 = clazz.getConstructor(paramTypeEmpty);
		    Object[] paramEmpty = {
    
    };
		    Person thisIsTestClassEmpty = con0.newInstance(paramEmpty);
		       
		    //getConstructor接受变长参数,以Class数组形式传入,告诉jdk我们要调用哪个构造器
		    Class[] paramType1 = {
    
    String.class};
		    Constructor<Person> con1 = clazz.getConstructor(paramType1);

		    //Constructor实例的newInstance同样也是接受一个变长参数,参数数组中类型要和getConstructor的类型对应
		    Object[] params1 = {
    
    "ParamOne"};
		    Person thisIsTestClass1 = con1.newInstance(params1);

		    //params2中的数据类型要和paramTypes2中的类型相对应
		    Class[] paramTypes2 = {
    
    String.class,Integer.class};
		    Constructor<Person> con2 = clazz.getConstructor(paramTypes2);
		    Object[] params2 = {
    
    "ParamOne",2};
		    Person thisIsTestClass2 = con2.newInstance(params2);
		}catch(Exception e){
    
    
	        e.printStackTrace();
		}
	}
}

  测试结果:

无参构造方法
无参构造方法
一个参数构造方法
两个参数构造方法

9.4 序列化

  序列化,简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。
  序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
  利用序列化创建对象的示例:

/*实体类*/
public class Person implements Serializable{
    
    
	
	private static final long serialVersionUID = 1L;
	String name;
	int age;
	
	public Person(String name,Integer age) {
    
    
		this.name = name;
	    this.age = age;
	}


	@Override
	public String toString() {
    
    
	    String s = "姓名是:"+this.name+",年龄是:"+this.age;
	    return s;
	 }
}
/*测试类*/
    public static void main(String[] args) {
    
    
        String filename = "E:/serialize.txt";
        serialize(filename);
        reverse_serialize(filename);
    }

    /**
     * 序列化
     */
    public static void serialize(String filename) {
    
    
        try {
    
    
            OutputStream fos = new FileOutputStream(filename);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeInt(12345);
            oos.writeObject("Today");
            oos.writeObject(new Person("Jack",20));
            oos.close();
            fos.close();
        }catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    /*反序列化*/
    public static void reverse_serialize(String filename) {
    
    
        try {
    
    
            InputStream in = new FileInputStream(filename);
            ObjectInputStream obj = new ObjectInputStream(in);
            int i = obj.readInt();
            String str = (String)obj.readObject();
            Person person = (Person)obj.readObject();
            System.out.println(i);
            System.out.println(str);
            System.out.println(person);
            obj.close();
            in.close();
        }catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

  测试结果:

12345
Today
姓名是:Jack,年龄是:20

9.5 创建对象的一些好方式

  假设有这样一个场景,现在要构建一个大型的对象,这个对象包含许多个参数的对象,有些参数有些是必填的,有些则是选填的。那么如何构建优雅、安全地构建这个对象呢?
1、构造函数重载
  这种做法也在JDK源码中很常见,简单示例:

public Person(String name, int age) {
    
    
	this.name = name;
	this.age = age;
}
public Person(String name, int age, int height) {
    
    
	this.name = name;
	this.age = age;
	this.height = height;
}

  上面的写法有下面的缺陷:

1、导致类过长。
2、有些参数组合无法重构。因为 Java 中重载是有限制的,相同方法签名的方法不能构成重载,编译时无法通过。譬如包含(name, age, school)和(name, age, hobby)的构造函数是不能重载的,因为 shcool 和 hobby 同为 String 类型。

  所以上面的代码还得改改,用一些setXXX方法,简单示例:

public void setHeight(int height) {
    
    
	this.height = height;
}
public void setSchool(String school) {
    
    
	this.school = school;
}

  这样修改后,在单一线程的环境中这确实是一个非常好的构建对象的方法,但是如果是在多线程环境中仍有其致命缺陷。在多线程环境中,这个对象不能安全地被构建,因为它不是不可变对象。一旦Person 对象被构建,我们随时可通过 setXXX()方法改变对象的内部状态。
2、Builder 方式
  使用创建者模式时,可以将要创建的对象用final修饰,使得其不可变。用该方式,有以下好处:

多个参数是可选的、顺序任意;
对象不可变。

  建造者模式可以参考如下文章:设计模式(二)创建型模式介绍及实例

十、Object类

10.1 Object常用方法

  Object是是所有类的父类,如果一个类没有用extends明确指出继承于某个类,那么它默认继承Object类。JDK8版本中,Object方法:

public Object();
private static native void registerNatives();
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj);
protected native Object clone();
public String toString();
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout);
public final void wait(long timeout, int nanos);
public final void wait();
protected void finalize();

10.1.1 构造方法

  Object类中没有显示的提供构造方法,由编译器默认提供。

10.1.2 registerNatives

    private static native void registerNatives();
    static {
    
    
        registerNatives();
    }

  该方法的主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦。该方法写在static块中,在类首次进行加载的时候执行。

10.1.3 getClass

public final native Class<?> getClass();

  此方法返回类运行时的类型,示例:

public class JavaTest {
    
    
	public static void main(String[] args)  {
    
    
		System.out.println((new JavaTest()).getClass());  //class com.test.JavaTest
	}
}

10.1.4 hashCode

public native int hashCode();

  先看例子:

public class JavaTest {
    
    
	public static void main(String[] args)  {
    
    
		System.out.println((new JavaTest()).hashCode());  //2018699554
	}
}

  该方法返回对象的哈希码,是一个整数。这个方法遵守以下三个规则:

  1. 在java程序运行期间,若用于equals方法的信息或者数据没有修改,同一个对象多次调用此方法,返回的哈希码是相同的。
  2. 如果根据equals方法,两个对象相同,则这两个对象的哈希码一定相同。
  3. 假如两个对象通过equals方法比较不相同,那么这两个对象调用hashCode也不是要一定不同,相同也是可以的。

  在实际使用中,要尽量保证对于不同的对象产生不同的哈希码。hashCode的典型实现是将对象的内部地址转为一个整数,但是这种实现技术不是Java语言必须要采用的。

10.1.5 equals

    public boolean equals(Object obj) {
    
    
        return (this == obj);
    }

  equals方法主要是比较两个对象是否相同,Object中的equals方法比较的是对象的地址是否相同。
  在实际开发中,对于实体类,一般要重写equals(比较对象的内容而不是地址)和hashCode(尽量让不同的对象产生不同的哈希值)方法。

10.1.6 clone

protected native Object clone() throws CloneNotSupportedException;

  浅拷贝一个对象。

10.1.7 toString

    public String toString() {
    
    
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

  示例:

public class JavaTest {
    
    
	public static void main(String[] args)  {
    
    
		System.out.println((new JavaTest()).toString());  //com.test.JavaTest@7852e922
	}
}

  可以看到Object中toString方法的实现是返回类的名称(权限定名称)加上@,然后 加上此类的哈希码的16进制表示。

10.1.8 wait

  在Object中存在三种wait方法:

public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
    
    
        if (timeout < 0) {
    
    
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
    
    
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
    
    
            timeout++;
        }

        wait(timeout);
    }
    public final void wait() throws InterruptedException {
    
    
        wait(0);
    }

  wait()和wait(long timeout, int nanos)都在在内部调用了wait(long timeout)方法。这个方法是线程方面的,在线程相关文章会详细介绍。

10.1.8 notify/notifyAll

	public final native void notify();
	public final native void notifyAll();

  这两个方法是唤醒线程,也会在后续线程相关文章中详细介绍。

10.2 hashCode 与 equals 【重要】

  hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
  散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
  hashCode()与equals()的相关规定:

1、如果两个对象相等,则hashcode一定也是相同的;
2、两个对象相等,对两个对象分别调用equals方法都返回true;
3、两个对象有相同的hashcode值,它们也不一定是相等的。

  因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖

  • Java中的hashCode()的作用
      hashCode()的作用是为了提高在散列结构存储中查找的效率,在线性表中没有作用;只有每个对象的 hash 码尽可能不同才能保证散列的存取性能,事实上 Object 类提供的默认实现确实保证每个对象的 hash 码不同(在对象的内存地址基础上经过特定算法返回一个 hash 码)。
      在 Java 有些集合类(HashSet)中要想保证元素不重复可以在每增加一个元素就通过对象的 equals 方法比较一次,那么当元素很多时后添加到集合中的元素比较的次数就非常多了,也就是说如果集合中现在已经有 3000 个元素则第 3001 个元素加入集合时就要调用 3000 次 equals 方法,这显然会大大降低效率,于是 Java 采用了哈希表的原理,这样当集合要添加新的元素时会先调用这个元素的 hashCode 方法就一下子能定位到它应该放置的物理位置上(实际可能并不是),如果这个位置上没有元素则它就可以直接存储在这个位置上而不用再进行任何比较了,如果这个位置上已经有元素了则就调用它的 equals 方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址,这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次,而 hashCode 的值对于每个对象实例来说是一个固定值。
      关于“重写equals()方法就必须重写hashCode()方法的原因”,可以参考如下文章:详解重写equals()方法就必须重写hashCode()方法的原因

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/107603547