Java中的类(基础详解)
Posted _房似锦_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的类(基础详解)相关的知识,希望对你有一定的参考价值。
文章目录
java中最常见的就是类,可以说,Java程序是由一个一个的类组成的
在C++中,我们只有在面向对象编程的时候才会用到类,一般想实现某一个功能可以写一个函数。
可以有多个类,但只能有一个public类
在一个 .java 程序中,可以出现多个类,但有且仅有一个类是 public 并且这个public类的名字必须和文件名相同,看图片来的快,有图有真相
我们看到此时主类的名字和文件名相同,此时没有报错,如果我改一下主类的名字,情况如下
很明显的报错,下面我们来看看错误原因
翻译过来就是:hello 类是public的,应该被声明在一个文件名叫 hello.java 的文件中。
下面我们来看一下一个java中有多个类型情况:
这里就是记住java中可以有多个类,但是只能有一个public类。
因为每一个java程序运行的时候都会先执行public这个类,而且只执行public类中的代码,如果写了其他的类但是在public类中没有用到,就不会执行其他的类,但是这个类的写法必须正确;如果写了其他的类并在public类中使用了其他的类,那么也会执行其他类的相应代码。
而且除public类之外的其他类也可以写在public类的后面,即使在主类中要调用这个类
代码:
public class Main
public static void main(String args[])
System.out.println("hello world!");
person p=new person(); //创建一个person类
p.sayhello(); //调用类其中的一个方法
class person
String name;
int age;
void sayhello()
System.out.println("嗨嘿嗨");
void printages()
System.out.println(age);
我们可以看到在程序截图中可以看到有 1 usage 或者 2 usages ,但是在单纯的代码片段中没有。
这个单词 usage 的意思是 用法,惯例
也可以认为是 仓库中本Jar被其他Jar依赖引用的次数。
其实就是某个变量或者函数被使用的次数,这个其实不用管它,影响不大。
类 = 字段+方法
我们举一个类的例子:
class person
//name和age属于类中的字段
String name;
int age;
//sayhello()函数属于类的方法
void sayhello()
System.out.println("嗨嘿嗨");
- 字段 是类的属性,是用 变量来表示的。
我们可以认为类中的变量都属于类的字段,字段有成为 域,域变量,属性,成员变量等 - 方法 是类的操作和功能,是用函数来表示的。
类的构造函数
类的构造函数可以用来给一个类的数值赋一个初值,用来初始化(new)该类的一个新的对象。
而且构造函数和类名同名,并且不需要写返回值类型。
class person
String name;
int age;
person(String s,int a) //类的构造函数
name=s;
age=a;
void sayhello()
System.out.println("嗨嘿嗨");
void printages()
System.out.println(age);
我们没写构造函数的时候
如果我们自己没有写构造函数,那么程序会自动生成一个默认构造函数,这个默认构造函数没有参数,函数中也没有任何语句,也就是相当于什么都不做。
默认生成的类似于这样
person()
其实就是什么都没做。
在没写构造函数的时候,初始化一个新对象的时候,不需要写参数
运行结果
如果我们自己写了构造函数
那么程序就不会再生成默认构造函数了,在初始化一个对象的时候,就使用咱们自己写的构造函数了,并且如果自己写的构造函数中有参数,必须要加上参数,否则报错,如果本身就没写参数,那么可以不写。
还是看图片来的快
构造函数中 this 的使用
- this 指当前这个对象实例本身
比如说,age=和 this.age 是一样的,都是可以运行的。
void printages()
System.out.println(age);
void printages()
System.out.println(this.age);
- this 还可以用来解决 局部变量 和 域 同名的问题,比如说
这样写也是可以运行的,那么this.name指的就是域变量,name指的就是参数变量。
person(String name,int age)
this.name=name;
this.age=age;
- 在构造函数中,this 可以调用另一种构造方法,并且这条调用语句必须放在第一句。
person()
this()
...
类的修饰符 / 控制符
作用:可以修饰类,也可以修饰类中的成员(字段,方法)
第一类:访问修饰符
public, private, protected,
- private:只能在同一个类中被访问。
- protected:可以在同一个类中,同一个包中,和不同包中的子类中被访问。
- public:public的访问范围最广,一般都可以访问。
- 如果不加修饰符,则只能在同一个类和同一个包中,这2中情况下访问。
类的访问控制符为public或者默认。
如果类用public修饰,则该类可以被其他类所访问。
如果类是默认访问控制符,则改类只能被同包中的类访问。
第二类:其他修饰符 / 非访问控制符
abstract, static, final
- static:静态的,非实例的,类的
可以修饰内部类,也可以修饰成员 - final:最终的,不可改变的
可以修饰 类,成员,局部变量 - abstract:抽象的,不可实例化的
可以修饰 类,成员
static字段
- 静态字段最本质的特点是:它们是类的字段,不属于任何一个对象实例。
- 它不保存在某个对象实例的内存区间中,而是保存再类的内存区域的公共存储单元。
- 类中的static变量可以通过了类名直接访问,也可以通过对象实例来访问,两种方法的结果是相同的。
因为这个static变量是存在最原本的类中的,它本身和用它来实例化的对象都可以访问。
例如System类的in和out对象,就是属于类的域,直接用类名来访问,即System.in和System.out 。
再举一个下面的例子:
import java.io.*;
public class Main
public static void main(String args[])
person p=new person("ycy",16); //创建一个person类
System.out.println(p.age); //这里用一个实例化的对象来访问类中static字段
System.out.println(person.age); //这里可以用类名来直接访问类中static字段
class person
String name;
static int age;
person(String name,int age)
this.name=name;
this.age=age;
输出结果:
final
- final类:如果一个类被final修饰符所修饰和限定,说明这个类不能被继承,即不会拥有子类
- final方法:final修饰符所修饰的方法,是不能被子类所覆盖的方法。
- final字段和final局部变量:它们的值一旦给定,就不能更改。并且它们是只读量,它们能且只能被赋值一次,不能多次赋值。
- 如果一个字段被 static final 同时修饰时,它可以表示常量。如果不给定初始值,则按默认值进行初始化(数值为0,boolean类型为false,引用类型为null)
abstract
- 凡是用abstract修饰符修饰的类被称为抽象类。
- 抽象类不能被实例化。
- 抽象类方法在子类中必须被实现,否则子类仍然是abstract的。
Java中的垃圾回收算法详解
一、前言
??前段时间大致看了一下《深入理解Java虚拟机》这本书,对相关的基础知识有了一定的了解,准备写一写JVM
的系列博客,这是第二篇。这篇博客就来谈一谈JVM
中使用到的垃圾回收算法。
二、正文
?2.1 什么是垃圾回收
??在正式介绍垃圾回收算法前,先来说说什么是垃圾回收。这里所说的垃圾主要指的是已经不会再继续使用的对象,当然也有可能是其他,比如不再使用的类以及常量,但主要还是指对象,所以以下算法将介绍对象的回收。所以垃圾回收的含义就是:将内存中已经不会被使用的对象(或类和常量)清除,释放内存空间。
??JVM
的内存模型分为五个部分,其中堆内存的唯一目的就是存放对象,对象也基本上都是存放在堆内存中。堆中,为了方便进行垃圾回收,一般会将内存分为两个部分:
- 新生代:用来存放生命周期短的对象。由于这一块内存中的对象存活时间较短,所以频繁发生垃圾回收,而且每次回收一般都能释放大量空间;
- 老年代:用来存放生命周期长的对象。新生代中存活了较长时间的对象会被迁移到这里(当然,对象进入老年代不仅仅只有这一个方法),所以这里存放的对象生命周期一般较长,所以这一块区域发生垃圾回收的频率较低,释放的空间也较少;
??下面正式开始讨论JVM
中的垃圾回收算法。
?2.2 如何识别垃圾
??进行垃圾回收的第一步就是找到垃圾(我们这里主要以对象为例),也就是无法被使用的对象。对象在什么情况下无法被使用?很简单,没有引用指向这个对象,我们自然无法使用它,比如看下面这段代码:
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
a = null;
}
??上面的代码中,我创建了一个对象,并使用变量a
指向这个对象,但是在这之后,我又将null
赋给了a
,这会出现什么情况?不难发现,我们已经无法使用这个对象了,它已经丢失了,因为我们已经无法通过任何变量去调用这个对象,但是它依然在内存中。此时,这个对象占用着内存就是白白浪费资源,我们希望它被清除。所以,我们可以想到,当一个对象没有引用指向它时,就可以认为他是一个垃圾对象了。
?(1)引用计数法
??引用计数法就是通过引用来识别无用对象。我们记录每一个对象的引用个数,若有新的变量引用一个对象时,这个对象的引用个数加1;若一个引用失效时,引用的个数减1,而引用个数为0的对象,即可作为垃圾被回收。这里要注意,若这些垃圾对象的成员变量引用了其他对象,则当垃圾对象被释放时,它的这个引用自然就失效了。
??这个算法实现简单,效率也高,但是,它并没有被用在主流的Java
虚拟机中,因为它有一个很大的缺陷——很难解决循环引用的问题。什么是循环引用,看下面一段代码:
public class Main {
private Object obj;
public static void main(String[] args) {
Main m1 = new Main();
Main m2 = new Main();
// 循环引用
m1.obj = m2;
m2.obj = m1;
m1 = null;
m2 = null;
}
}
??上面这段代码中,创建了两个对象m1
和m2
,它们都有一个属性obj
。而m1
的obj
指向了m2
,而m2
的obj
指向了m1
。多个引用形成一个环,这就是循环引用。这对于使用引用计数算法的垃圾回收器来说有一个问题,即上面的代码最后,m1
和m2
都置为了空,它们指向的两个对象已经无法再使用了,但是由于这两个对象相互引用,导致它们的引用计数并不为0
,所以垃圾回收器不会将它们判别为无用对象。正是因为这个问题的存在,Java
中的垃圾回收器基本上不使用这个算法。
?(2)可达性分析法
??可达性分析法是Java
垃圾回收中判别无用对象的主要方法。这个方法的步骤是,从根节点对象出发,使用DFS
或BFS
算法,沿着引用递归遍历,而无法被遍历到的对象,就是无法再被使用的对象,可以被垃圾回收器回收。所谓的根节点,就是我们能够直接使用的引用类型变量,如:
- 方法中的参数或局部变量;
- 类的静态成员或非静态成员;
- 代码中的常量;
??这种方法的效率相对于引用计数来说相对复杂,而且效率较低,但是解决了循环引用的问题,是Java
垃圾回收中主要使用的方法。
?2.3 如何释放垃圾
??释放垃圾指的就是清除无用对象,释放它们所占的内存空间,方便继续使用。这里主要介绍三种方法:
- 标记—清除算法;
- 复制算法;
- 标记—整理算法;
??这三种算法根据具体情况的不同,搭配使用,才能发挥最好的效果。下面就来一一介绍。
?(1)标记—清除算法(Mark-Sweep)
??标记—清除是以上上面三种算法中最基础的一种,为什么说它是最基础的,因为它的原理非常简单。故名思意,这个算法分为两个步骤:(1)标记;(2)清除。
- 标记:标记指的就是我们上面所说的可达性分析,采用之前所说的可达性分析算法遍历对象,所有不可达的对象将被标记为垃圾,等待回收;
- 清除:这一步很简单,直接释放垃圾对象所占内存空间;
??这个算法有两个的问题:
- 效率较低,标记和清除这两个步骤的效率都比较低,清除的效率低是因为需要扫描整个内存空间,逐个释放对象所占内存;
- 使用这个算法清除垃圾后,将会造成很多内存碎片,所以可能出现剩余内存较多,但是没有较大的连续空间,导致大对象无法被分配空间,而再次触发垃圾回收;
??我们通过两张对比图来看看这个算法的效果。通过下面这张图我们可以看到,在垃圾回收后造成了很多的内存碎片。
?(2)复制算法(Copying)
??为了解决效率较低以及产生内存碎片的问题,有人提出了一个新的算法——复制算法。这个算法的原理是:将内存分为两个相等大小的区域,一块存放对象,一块保留。当存放对象的那块区域无法再分配空间时,将所有仍然存活的对象复制到保留的那块区域中,然后直接释放当前正在使用区域的全部内存。这样一来,仍然存活的对象被放进保留区,而垃圾对象也被释放了。同时,之前被使用的空间被清空后,成了新的保留区,而之前的保留区成了被使用的空间,就这样不断循环使用两个空间。
??我们之前提过,堆内存被分为新生代和老年代。在新生代中,每次垃圾回收都可以释放大量的对象,只有少部分存活,所以只有少部分对象要被复制到保留区中,这也意味着复制并不会太耗时。除此之外,直接释放被使用的空间的全部内存,比一段一段释放的效率也要高很多。同时,对象被复制到另外一个区域时,会被整齐地摆放,所以不会出现内存碎片,所以能够更简单地分配空间。所以,复制算法的效率要远远高于标记—清除算法。以下是一张复制算法的演示图:
??但是,这里存在一个问题,复制算法将内存区域划分为相等的两部分,这也意味着每次都有一半的空间无法被使用,这未免也太浪费了。所以,对于空间的划分,需要做出一些改进。IBM
公司的研究表明,98%
的对象存活时间都非常的短暂,所以,完全没有必要保留一半的空间供复制使用。在实际实现中,会将空间划分为三块区域,一块较大的Eden
空间,以及两块较小的Survivor
空间。在为新对象分配空间时,首先会将其分配到Eden
空间中,若Eden
空间无法再分配空间时,将会触发垃圾回收,此时,会将Eden
空间中的存活对象复制到其中一块Survivor
空间中,然后清空Eden
空间。当Eden
空间再一次因无法分配空间而触发垃圾回收时,则会将Eden
空间中的存活对象,以及上一次被复制进Survivor
空间中的存活对象,都复制到另一块Survivor
空间中,然后将Eden
和上一块Survivor
清空。也就是说,交替地使用两块Survivor
空间,来存放垃圾回收中任然存活的对象。而在具体实现中,这三个空间的比例一搬是8:1:1
,即是说只有10%
的空间无法被使用。
??可以看出,这个算法在大部分对象的生命周期都短时,效率会非常高,但是若大部分对象的生命周期都很长,将不再适用,所以这个算法一般只被用在新生代中。这里我们不得不考虑一个问题,当我们使用了上面说的将内存划分为三块的这种方式时,可能会出现一个问题:如果在某次垃圾回收过后,仍然有大量的对象存活,此时一个Survivor
空间不够存放这些对象怎么办?这时候就需要有另一个空间来做担保了,当这种情况发生时,会将这些对象放入另一个空间中,那个空间就叫做担保空间。就像我们去银行贷款,需要有一个担保人,当贷款人不能偿还时,由担保人代为偿还。以上算法是用在新生代中,而所谓的担保空间,实际上就是老年代。老年代为这个算法提供了担保,但是在大部分情况下,Survivor
都是能够满足需求的。
?(3)标记—整理(Mark-Compact)
??由于老年代中的对象一般存活时间都比较长,所以并不适合在老年代使用上面的复制算法进行垃圾回收。而有人根据老年代的特点,提出了标记—整理算法,注意看清楚,这里是整理,而不是第一种算法中的清除。这个算法也分为标记和整理两个步骤,标记这个步骤和第一个算法是一样的,关键是整理步骤。所谓的整理,就是将内存中还存活的对象向一边移动,直至这些对象相互靠拢,整齐排列,然后直接清除不属于这一部分的全部内存。标记—整理的好处是解决内存碎片的问题。以下是这个算法的演示图:
?(4)分代收集算法
??分代收集算法并不是什么新思想,而是对上面三种算法的综合使用。前面也提过,为方便垃圾回收,一般将堆内存分为新生代和老年代两个部分。
- 对于新生代而言,这一块区域中的对象存活时间短,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法,同时以老年代作为这个算法的担保空间;
- 对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时
Survivor
需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法;
三、总结
??上面对JVM
中的垃圾回收算法做了一个比较详细的介绍,相信看完这一篇博客会对这部分内容有更深的理解。但是,归根到底,上面的内容只是理论,接下来我将写一篇博客,来讲讲JVM
具体如何分配和释放对象,作为JVM
系列博客的第三篇。
四、参考
- 《深入理解Java虚拟机》
以上是关于Java中的类(基础详解)的主要内容,如果未能解决你的问题,请参考以下文章