Java基础知识(三)面向对象、类和对象、封装继承多态、构造方法、内部类、包装类
一、面向对象与面向过程
1.1 什么是面向对象
关于面向对象与面向过程,个人的理解是,这是对解决问题方案的两种看待方式:
1>面对对象更关注的是数据的调用方和接收方(也就是对象),解决问题变成了对象执行某一个或某一系列动作。
2>面向过程更关于解决问题这个环节本身,是将一个个环节封装起来,也就是函数。
简而言之,面向对象中,实现一个功能的载体是对象,由对象去调用对应的方法即可。在面向过程中,某个功能的实现是由一系列函数组成的,某个函数中常常包含其他函数,由这些函数依次调用来实现复杂的功能。
要了解面向对象,需要先了解对象,对象的特点如下:
1.2 面向对象与面向过程的例子
关于面向过程与面向对象的具体实现方式,借一个小例子来说明。比如在热门游戏《英雄联盟》中,每个英雄都有不同的皮肤,不同的实现方式:
1>如果用面向对象的方式来实现,就可以创建两种对象:英雄人物对象和皮肤对象,然后将两者组合在一起,就可以达到创建穿不同皮肤的不同英雄对象的目的。
2>如果用面向过程的方式来实现,就需要分别创建穿不同皮肤的不同英雄人物对象!
在该例子中,面向对象实现方式的好处是"英雄"“皮肤"分离,从而提高了英雄穿戴皮肤的灵活性,从软件工程的角度考虑,就是"可维护性"比较好,因为“英雄” 和"皮肤"两个对象的耦合度比较低。软件工程追求的目标之一就是可维护性,这也是面向对象的好处之一,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。
1.3 面向对象与面向过程的区别
用专业的话总结一下面向对象与面向过程的区别:
1>面向过程
优点:性能比面向对象高,因为在面向对象中,类调用时需要实例化,开销比较大,消耗资源多。比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。
2>面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:性能比面向过程低。
二、类与对象的关系
类是一组事物共有特征和功能的描述。类是对于一组事物的总体描述,是用面向对象思想进行设计时最小的单位,也是组成项目的最基本的模块。对象是类的实例,类是对象的模板,如碳酸饮料是类,可口可乐、百事可乐、雪碧是具体的对象。
2.1 成员变量和成员方法
类中一般至少两种内容:成员变量和成员方法。成员变量也叫属性,是组成的对象的数据,比如Student对象,所具备的属性应该有姓名、各科目分数、所在班级、所在学校等;成员方法是操纵这些数据的行为,比如文理分科后调班、考试后取得了不同的分数等。
关于局部变量和成员变量,简单区别如下:
1. 作用范围
成员变量作用于整个类中,其声明是在类之内、方法之外;局部变量作用于方法中或者语句中,其声明是在方法之内。
2. 在内存中的位置
成员变量在堆内存中,因为对象的存在,才在内存中存在;局部变量存在栈内存中。
当对象通过new的方式被创建出来时,对象实体存在于堆,对象的成员变量在堆上分配空间,但对象里面的方法是没有出现的,只出现方法的声明,方法里面的局部变量并没有创建。等到对象调用此方法时,为了加快运行的速度,方法中的局部变量才会在栈中创建,所以,方法中的局部变量是在栈内的。
2.2 对象的存储
当使用new的方式创建对象时,会先在堆内存中开辟一块区域,存放该对象的具体数据(即成员变量),然后在栈内存中生成一个引用,指向堆内存中生成的具体对象。如下:
需要注意的是类变量(静态变量)存在于方法区。
关于不同内存区域的特点,此处简单介绍,后续会有专门的文章进行分析,区别如下:
内存区域 | 特点 |
---|---|
栈 | 存放局部变量 不可以被多个线程共享 空间连续,速度快 |
堆 | 存放对象 可以被多个线程共享 空间不连续,速度慢,但是灵活 |
方法区 | 存放类的信息:代码、静态变量、字符串常量等等 可以被多个线程共享 空间不连续,速度慢,但是灵活 |
2.3 匿名对象
在Java中,有时会创建匿名对象,匿名对象就是没有名字的对象,在创建对象时,只通过new的动作在堆内存开辟空间,却没有把堆内存空间的地址值赋值给栈内存的某个变量用以存储。
由于使用匿名对象不需要分配栈内存,且无需进行引用指向,在大量创建对象的时候能够节约很多的栈空间,且数量越多越明显。
使用匿名对象的好处是:当匿名对象使用完毕就是垃圾,垃圾回收器会在空闲时对匿名对象进行回收,节省栈内存空间。
三、封装、继承与多态
3.1 封装
在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。
3.1.1 封装的优点
封装的优点:
1>隔离变化,便于使用
此处常见的是:某个类中某一功能的实现,该类自己知道就行了,即便该功能的实现方式变化了,外部调用者也仍不知道该功能实现的细节,这样就达到了“隐藏细节,隔离变化的目的”。
2>提高代码复用性
这个好处较容易理解,当开发者A开发了一个复杂的功能,封装到functionA中,那么开发者B就可以直接调用functionA方法,不用再重新实现该功能。
3>提高安全性
此类好处,最常见的例子是对私有属性的封装。
3.1.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());
student.setGrade(90);
System.out.println("该学生的分数是:"+student.getGrade());
}
}
此时的测试结果为:
分数参数不合法,应该是0-100的整数
该学生的分数是:0
该学生的分数是:90
从上面结果可以看出,修改后的代码可以检测非法参数,通过用调用方法而不是直接修改属性的方式来修改学生的分数,达到了提高Student类成员属性安全性的目的。
3.1.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.2 继承
Java支持单继承,继承最大的优点是提高代码复用性,通常的做法是父类中定义公共的共性功能,不同的子类实现不同的差异功能。
继承是最简单,也是最常见的类与类之间的一种关系。除此之外,还有聚合、组合和依赖关系,后面有机会单独写文章分析,此处简单介绍:
1>聚合
聚合表示的是has-a的关系 : 父类包含子类,子类可以独立于父类存在,该方式体现的是整体与部分的单向关系,如汽车中包含引擎和轮胎。
2>组合
组合也是关联关系的一种特例,该方式体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合,如公司和部门。
3>依赖
一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A;比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。
在子类中要引用父类变量或方法的话,需要用关键字super,super代表父对象的引用。如果两个变量名一致,需要用this关键字,this代表对自身的对象的引用。
3.2.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.2.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();
}
}
测试结果:
创建手机
创建荣耀手机
播放手机铃声《荣耀》
3.2.3 抽象类
在父类中定义一个方法时,可以实现一个较完整的功能,子类不重写也能完全使用。当然,父类也可以完全不实现或者部分实现某个功能,此时父类需要子类去重写这个功能,对应实现功能的方法就要用abstract关键字来标识,此时的类就叫抽象类。
抽象类的特点:
1>抽象方法一定在抽象类中。
2>抽象方法和抽象类都必须被abstract关键字修饰。
3>抽象类不可以用new创建对象。
4>抽象类中的抽象方法要想被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用,如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。
一般情况下,抽象类就是将一些父类中完全不确定或部分不确定的部分抽取出来,封装到abstract方法中,让子类去实现。当然,抽象类也可以不定义抽象方法,这样只是为了不让该类创建对象。
由上述内容可知抽象方法与抽象类的关系:有抽象方法存在的类一定是抽象类,抽象类中不一定有抽象方法。
3.2.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 多态
多态指的是同一个行为具有多个不同表现形式或形态的能力。扩展开来说,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。这话有点抽象,先上例子:
/*家庭作业接口*/
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();
Homework homeworkB = new StudentB();
homeworkB.doHomework();
}
}
测试结果如下:
同学A写了一篇作文
同学B写了一套数学试卷
从上面的例子可以看出,同一个Homework接口类型的引用,调用同样的方法,却产生了不同的结果,这就是多态最直接的体现。所以,多态最常见的方式就是:父类的引用指向了自己的子类对象。
这个例子中也能看出多态存在的三个条件:继承、重写、父类引用指向子类对象。
四、构造方法
4.1 构造方法使用
构造函数是创建对象时调用的方法,主要作用是给新创建的对象赋予一些初始变量。当一个类中没有定义构造函数时,系统会默认给该类加入一个空参数的构造函数,当自己定义了构造函数后,就不再有默认的无参构造函数。
构造函数的特点:
1>函数名与类名相同
2>无返回值
3>无return语句
在对象较为复杂时,常常有多个构造函数,这些构造函数是以重载的形式存在的。
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();
}
}
测试结果:
做事前做一些准备工作
开始做事
五、关键字
5.1 this关键字
this关键字档表当前对象,用在构造函数期间,使用方式有两种:
1>给成员变量赋值,这种使用方式十分常见。示例代码如下:
public class Person {
String name;
int age;
public Person(String name,int age){
this.name=name;
this.age=age;
}
}
2>构造函数之间的互相调用,这种方式在原生的复杂对象,如String、File等对象中较为常用,以为他们的构造函数都不止一种。示例代码如下:
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;
}
}
5.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关键字,就成了静态代码块。静态代码块随着类的加载而执行,且执行一次,用于给类进行初始化,且优先于主函数、代码块、构造方法执行。这些方法的执行顺序,可以通过一个例子来看。示例代码如下:
/*实体类: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
5.3 final关键字
final也是在继承关系中,一个较为常用的关键字。final修饰变量时,变量不能被修改;final修饰方法时,方法不能被重写;final修饰类时,类不能被继承。
六、内部类
6.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();
}
}
测试结果:
外部类–>字符串
6.2 内部类的优点
内部类的优点:
1>内部类可以实现和外部类不同的接口,也可以继承和外部类不同的类,间接完成功能扩展。
2>内部类中的属性、方法可以和外部类重名,但并不冲突,因为内部类是具有类的基本特征的独立实体。
3>内部类利用访问修饰符隐藏内部类的实施细节,提供了更好的封装,除外部类,都不能访问。
4>静态内部类使用时可直接使用,不需先创造外部类。
6.3 内部类的分类
内部类可以分为四种:成员内部类、静态内部类、局部内部类、匿名内部类。
6.3.1 成员内部类
成员内部类是最常见的内部类,即一个类中嵌套了另一个类,无特殊修饰符。先看一个例子,来了解成员内部类的使用,包含内部类的类,示例代码如下:
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();
}
}
这个例子中可以看出内部类和外部类访问的一些简单规则:内部类可以直接访问外部类中的变量,外部类访问内部类变量时,则需要先创建内部类对象。当内部类和外部类中有相同名称的变量时,在内部类中需要用“外部类.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.属性或者方法。
6.3.2 静态内部类
顾名思义,是用static修饰的内部类。在静态内部类中,只能访问外部类中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() {
//被调用时会先加载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>静态内部类可以独立存在,不依赖于其他外部类。
6.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修饰的变量,不论是方法接收的参数,还是方法内的参数。
5>可以随意的访问外部类的变量和方法。
6.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>匿名内部类没有构造方法。
七、包装类
7.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 |
7.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中的一种“语法糖”。“语法糖”是指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。
7.3 包装类和基本类型的互相装换
其实在上一小节已经写了两者互相转换的规则,简单列一下:
基本类型 | 基本类型–>包装类 | 包装类–>基本类型 |
---|---|---|
byte | new Byte | byteValue |
short | new Short | shortValue |
int | new Integer | intValue |
long | new Long | longValue |
float | new Float | floatValue |
double | new Double | doubleValue |
boolean | new Boolean | booleanValue |
char | new Character | charValue |
示例代码如下:
/*基本型转换为包装类对象*/
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
7.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);
Integer int3 = 200;
Integer int4 = 200;
System.out.println(int3 == int4);
测试结果为:
true
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对象时,会创建相同的对象;否则会创建出不同的对象。
本文地址:https://blog.csdn.net/m0_37741420/article/details/107603547
上一篇: 稀疏数组与普通数组的转化,将数组存进文件与从文件中读取出来
下一篇: 这样写的代码不用担心代码评审