盒子
盒子
Posts List
  1. 前言
  2. Java中的类
  3. Java中类的关系
  4. Java类的生命周期
  5. 总结

Android 学习之路--Java基础(三)

作者Talent•C
转载请注明出处

前言

今天主要学习一下Java中的类及与类相关的知识,在Java开发中可以说类是我们时时刻刻都在使用的东西,也是面向对象的重要组成,废话不多说进入今天的正题。

Java中的类

什么是类?
在面向对象的编程语言中,类是一个独立的程序单位,是具有相同属性和方法的一组对象的集合。

类的组成格式

访问权限修饰符 修饰符 class 类名
{
类中的属性变量
类中的行为方法
}
例如:
public class Person
{
private String name;
private int age;
public void sayHello()
{
System.out.print(“你好”);
}
}

我们来了解一下Java中的几种权限修饰
(1) friendly: 默认权限,在包内访问。
(2) Public: 共有的,所有地方都可以访问;
(3) Private: 私有的,只有对象本身可以访问;
(4) Protected: 保护的,只有对象本身与其子类可以访问;

一个类中可以有哪些东西?
Java中的类中可以存在, 构造方法、 属性 、方法 、内部类、块。

构造方法:

  • 构造器必须与类同名(如果一个源文件中有多个类,那么构造器必须与公共类同名)
  • 每个类可以有一个以上的构造器
  • 构造器可以有0个、1个或1个以上的参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作一起调用

属性:
成员变量是类的属性, 例如一个人,他有很多属性,如名字,性别等等都是这个人属性
声明的一般格式为:
[变量修饰符] <成员变量类型> <成员变量名>
变量修饰符:public、protected、private、和默认(frieddlly)
方法:
成员方法成员方法定义的类的操作和行为,一般形式为:
[方法修饰符] <方法返回值类型> <方法名>([<参数列表>])
{
方法体
}
成员方法修饰符主要有public、private、protected、final、static、abstract和synchronized七种,前三种的访问权限、说明形式和含义与成员变量一致。
与成员变量类似,成员方法也分为实例方法和类方法。如果方法定义中使用了static ,则该方法为类方法。

内部类:
内部类是声明在类体中的类,包含内部类的类往往被称为外部类,内部类作为外部类的成员存在,可以像方法一样使用外部类的属性和方法,同时内部类也有类的特征,可以声明属性、方法、构造方法等,内部类也需要创建对象才可以使用其中的方法等。
代码示例: Person 类中含有一个内部类 Pen

public class Person
{
private String name;
private int age;
public pen p; //内部类 笔类对象
public Person()
{
p = new pen();
}
//内部类 笔类
public class pen {
public void write (String text)
{
System.out.print(text);
}
}
}

块:
块,在时间运用中可能不太常见,但也有部分程序中会使用到它,块分为静态块和实例块。

代码示例
{
实例块
}
static {
静态快
}

实例块不能直接被使用,每次调用任何构造方法创建对象时,在调用构造方法前,都会调用实例块的代码,所以实例块常常用来包含所有构造方法所需要执行的代码,而这些功能只能在构造方法前执行。静态快与实例块的区别是,静态快只能执行一次,而实例块每次创建对象都会执行,静态块往往用来包含该类必须执行且只能执行一次的功能代码。

Java中,类文件是以 .java为 后缀的代码文件,在每个类文件中最多只允许出现一个 public类,当有 public类 的时候,类文件的名称必须和public类的名称相同,若不存在public,则类文件的名称可以为任意的名称(当然以数字开头的名称是不允许的)。
在类内部,对于成员变量,如果在定义的时候没有进行显示的赋值初始化,则Java会保证类的每个成员变量都得到恰当的初始化:
1)对于 char、short、byte、int、long、float、double 等基本数据类型的变量来说会默认初始化为 0boolean 变量默认会被初始化为 false);
2)对于引用类型的变量,会默认初始化为 null
如果没有显示地定义构造器,则编译器会自动创建一个无参构造器,但是要记住一点,如果显示地定义了构造器,编译器就不会自动添加构造器。注意,所有的构造器默认为 static 的。

当程序执行时,需要生成某个类的对象,Java执行引擎会先检查是否加载了这个类,如果没有加载,则先执行类的加载再生成对象,如果已经加载,则直接生成对象。
在类的加载过程中,类的static成员变量会被初始化,另外,如果类中有static语句块,则会执行static语句块。static成员变量和static语句块的执行顺序同代码中的顺序一致。记住,在Java中,类是按需加载,只有当需要用到这个类的时候,才会加载这个类,并且只会加载一次。

疑问一 、类中为什么只有构造函数,析构函数去哪里了呢?

C++程序设计中有构造函数与析构函数的概念,并且是内存管理技术中相当重要的一部分,而在Java语言中只有构造器(也可以称为构造函数)的概念,却没有析构器或析构函数的概念。这是因为,理论上JVM负责对象的析构(销毁与回收)工作。也就是之前讲到的垃圾回收的概念。那么Java语言中是否真的不存在与C++中析构函数职能类似的方法?其实Java语言中的finalize 方法与C++语言中的析构函数的职能就极为类似。finalize方法是Java语言根基类Object类中所包含的一个方法,这个方法是保护类型的方法 (protected),由于在Java应用中开发的所有类都为Object的子类,因此用户类都从Object对象中隐式地继承了该方法。因此,我们在Java类中可以调用其父类的finalize方法,并且可以覆盖自身继承来的finalize方法。虽然我们可以在一个Java类中调用其父类的finalize方法,但是由于finalize方法没有自动实现递归调用,我们必须手动实现,因此finalize函数的最后一个语句通常是super.finalize()语句。通过这种方式,我们可以实现从下到上finalize的迭代调用,即先释放用户类自身的资源,然后再释放父类的资源。通常我们可以在finalize方法中释放一些不容易控制,并且非常重要的资源,例如:一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。
Java中的析构方法finalizeOC 中的dealloc相同。

finalize方法最终是由JVM中的垃圾回收器调用的,由于垃圾回收器调用finalize的时间是不确定或者不及时的,调用时机对我们来说是不可控的,因此,有时我们需要通过其他的手段来释放程序中所占用的系统资源,比如自己在类中声明一个destroy()方法,在这个方法中添加释放系统资源的处理代码,当你使用完该对象后可以通过调用这个destroy()方法来释放该对象内部成员占用的系统资源。虽然我们可以通过调用自己定义的destroy()方法释放系统资源,但是还是建议你最好将对destroy()方法的调用放入当前类的finalize()方法体中,因为这样做更保险,更安全。在类深度继承的情况下,这种方法就显得更为有效了,我们可以通过递归调用destroy的方法在子类被销毁的时候释放父类所占用的资源。

注意:
我们为减少内存资源的浪费以及缩短响应时间,我尽量不在构造函数里创建、初始化大量的对象或执行某种复杂、耗时的运算逻辑。

Java中类的关系

1.关联关系
给定有关联的两个类,可以从一个类的对象得到另一个类的对象。关联有两元关系和多元关系。两元关系是指一种一对一的关系,多元关系是一对多或多对一的关系。两个类之间的简单关联表示了两个同等地位类之间的结构关系。换句话说就是类A中存在类B的对象,即类A与类B存在关联关系;
代码示例:

public class A
{
private B b;
}
public class B
{
}

关联关系分为: 单项关联与双向关联
单项关联,顾名思义就是A类中B类而B类没有A类;而双向关联就是B中页存在A类。

2.依赖关系
一个类中的某个行为(方法)需要通过另一个类去完成则两个类的关系为依赖关系。如A类中有个方法f在f,方法f中使用B类的去处理一些事情。
代码示例:

public class A
{
void showBClassInfo(B b) {
b.p();
}
}
public class B
{
void p ()
{
System.out.print(“依赖关系”);
}
}

关联与依赖的区别和联系?

  • 关联是”HAS”关系,依赖是”USE”关系;
  • 关联是一个类持有另一个类,而依赖是一个类并不持有另一个类只是使用另一个类;
  • 生命周期不同,关联关系中,如A类持有B类则B类需要等A类销毁后才会销毁;依赖关系中,A类依赖B类,使用B类后立即销毁B类,而不用等待A类销毁后才会销毁B类。

我们在日常编程时,大多数都是既有关联关系又有依赖关系。

3.继承关系
一个类在另一个类的基础上衍生过来,衍生出来的类不仅拥有其全部属性与方法,还可以扩展了新的属性和方法,那么衍生出来的类与原来的类存在继承关系,即衍生出来的类为子类,原来的类为父类。
Java中继承关系使用 extends 关键字表示。
Student 类继承上述中的 Person

public class Student extends Person
{
private int stuNo;
}

上述代码表示一个学生类继承 Person 类。

这么做的好处是什么?

  • 继承简化了人们对事物的认识和描述,能清晰体现相关类间的层次结构关系。
  • 继承提供了软件复用功能。若类B继承类A,那么建立类B时只需要再描述与基类(类A)不同的少量特征(数据成员和成员方法)即可。这种做法能减小代码和数据的冗余度,大大增加程序的重用性。
  • 继承通过增强一致性来减少模块间的接口和界面,大大增加了程序的易维护性。
  • 提供多重继承机制。从理论上说,一个类可以是多个一般类的特殊类,它可以从多个一般类中继承属性与方法,这便是多重继承。Java出于安全性和可靠性的考虑,仅支持单重继承,而通过使用接口机制来实现多重继承。

抽象类
抽象类,不能实例化,通过关键字 abstract 修饰。
代码示例: 定义一个树的抽象类 Tree

public abstract class Tree {
protected int yearsCount;
protected String name;
int getYearCount ()
{
return yearsCount;
}
public abstract String getName ();
}
//松树类继承自树类
public class Pine extends Tree
{
}

作用:

  • 一般抽象类都是作为父类使用,用来定义子类的方法和属性,子类继承后复用属性和方法。
  • 抽象类虽然不能实例化,但是可以作为数据类型使用。
  • 抽象类中可以存在实现好的方法也可以存在抽象方法。
  • 可以将子类的一些公关方法提取出来。

抽象方法:
Java中没有方法体的方法称为抽象方法,使用 abstract 关键字修饰。
有抽象方法的类一定是抽象类,但抽象类中不一定有抽象方法,抽象方法被子类实现,如果子类不实现,则编译会报错。

多态:
多态是声明形式一样,而实现方式却不同的特征,下面是Java中多态的三大体现:

  • 方法重载: 一个类中有多个方法名相同但参数不同的方法。
  • 方法覆盖: 子类中重新实现方法,方法名相同,参数相同,返回值页相同。
  • 多态参数: 多态参数是一种用的比较多的方式,即方法中的形式参数是父类型,而实际传递的是不同的子类型。

4.实现关系
严格来说,实现关系并不是类与类的关系,而是类与接口的关系,而接口的本质是一个特殊的抽象类,因此,实现关系也可以理解为一种类与类的关系,实现关系与集成关系类似。

接口:
Java中,可以使用 interface 关键字修饰。
代码示例:

public interface TalentC {
public abstract void showAllInfo();
}

接口的特点:

  • 没有属性变量,所有的方法都是抽象方法。
  • 可以看做是一种特殊的抽象类,接口编译后也会生成 .class 文件。
  • 可以有静态常量,如 public static int a = 100; 中的修饰符可以省略不写。

接口与类的关系:
使用关键字 implements 表示实现接口,一个类可以实现多个接口。
代码示例:松树类实现 接口类TalentC

public class Pine extends Tree implements TalentC
{
public void showAllInfo()
{
System.out.print(“实现接口TalentC类”);
}
}

由于Java中没有多继承,一个类只能继承一个父类,但同时可以实现多个接口,解决了类与类的单继承的局限性。虽然接口不可以实例化,但是可以作为数据类型使用,也可以作为多态形式参数使用。

注意:
Java中类的继承只能是单继承,接口与接口的继承可是多继承。

Java类的生命周期

我们先来了解jvm中几个重要的内存区域,这几个区域在类的生命周期中有很重要的作用。

方法区:Java虚拟机 中有一块内存区域专门用来存放已经加载的类的信息、常量、静态变量以及方法代码的内存区域。

常量池: 常量池也是方法区的一部分,主要存放常量和类中的符号引用等信息。

堆区: 用于存放类对象的实例。

栈区: 也叫 Java虚拟机栈, 是一个一个由栈帧组成,后进先出的栈式结构。栈帧中存储运行时产生的局部变量、方法出口信息,当调用一个方法时,虚拟机会创建一个栈帧存放这些数据,当方法调用完毕时,栈帧消失,如果方法中调用方法则继续创建新的栈帧。

除了以上四个内存区域之外,jvm中的运行时内存区域还包括 本地方法栈程序计数器,这两个区域与 Java类生命周期 关系不是很大,就不多说了。

Java类的生命周期:
一个 Java类 的源文件编译后产生.class的文件,也叫字节码文件,只有字节码文件才可以在 Java虚拟机 中运行,Java类 的生命周期就是一个 class文件 从加载到卸载的过程。

这一过程分为: 加载连接初始化使用卸载 共五个阶段。

加载:
Java虚拟机 将需要加载的类的信息加载到jvm的方法区中,然后在堆区实例化对象,作为方法区中这类的信息的入口;Java中类的加载方式比较灵活,通常的加载方式有两种:
(1)根据全路径名找到对应的 class文件,从中读取文件内容。
(2)从 jar 文件中读取内容。
另外还有几种方式,有不是特别常见这里就不介绍了。

加载类的时机各个 Java虚拟机 做法不同,但有一个原则,当jvm“预期”到一个类将要被使用时,就会在使用之前对这个类加载。如jvm在代码段中遇到一个类的名字,jvm在执行代码段之前并不能确定这个类是否被使用到,于是有些jvm会在执行这段代码前加载这个类,而有些则是真是使用到这个类时才会加载这个类,这取决于jvm的实现,如 “hotspot”就做到了真正使用到一个类时才会加载这个类。

加载阶段是 Java类 生命周期的第一个阶段,之后是连接阶段,不过有时连接阶段并不是等待加载阶段完毕后才进入连接阶段的,而是交叉进行的。但是有一个原则就是:

  • 连接阶段总是在加载阶段开始后进入的;
  • 加载阶段的完成总是在连接阶段完毕前完成的;

连接:
连接阶段就是为初始化做一些准备工作的,分为三个步骤进行: 验证准备解析;

(1)验证:
主要作用是验证一下这个类是否合法、是否为字节码文件、变量名和方法名是否重复、数据类型是否有效等,为了保证加载的类可以被jvm执行。

(2)准备:
为静态变量分配内存,对于非静态变量则不会分配内存;
jvm默认的初值是这样的:

  • 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0
  • 引用类型的默认值为null
  • 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 10,则准备阶段中a的初值就是10。

(3)解析:
这一步骤的任务是将常量池的符号引用转换为直接引用;如一个类有一个方法say(),我们在内存中查询这个方法say()是查询不到的,但jvm的这个步骤会将say()这个方法转换为指向这个方法的内存地址,通过这个内存地址我们就可以使用这个方法了。

如果上一个例子不太懂我们换一个通俗的列子就是,我们每个人都有一个身份证号:9876543210,我们如果直接使用这个身份证号是无法找到这个人的,我需要借助公安系统通过这个身份证号找到这个人,我就可以知道这个人名字、性别、住址等信息。

初始化: 创建对象,这个详细大家都已经很清楚了就不解释了;

使用: 使用这个阶段就更简单了, 如 Person 类对象 p 调用 say()方法;

卸载:
jvm会在方法区垃圾回收时对类进行卸载,也就是将方法区该类信息清空,执行到这一阶段,Java类 的生命周期全部完毕。

卸载的三个条件:

  • 该类的所有实例对象全部被回收,也就是Java堆中不存在任何该类的对象;
  • 加载该类的ClassLoader已经被回收;
  • 该类对应的java.lang.class对象没有任何地方被引用,无法再任何地方通过反射访问该类的方法;

总结

我们今天学习了Java中类及类的关系、类的生命周期;其中抽象类与接口很容易被一些初学者弄混;Java类不可以多继承,接口是都多继承的;弄清楚了类的生命周期有助于我们提高程序健壮性,避免一些不必要的问题出现。这里类的生命周期不是对象的生命周期,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况。

支持一下
扫一扫,支持Talent•C