JVM启动时的内存划分
栈内存:运行所有方法的。方法压栈或弹栈。也会存基础类型的变量、对象的引用变量。**回收:**当超过变量的作用域后,会自动释放,该空间即可以另作他用
堆内存:所有使用new 关键字创建的均放置于其中;**回收:**由jvm gc来回收
方法区:又会划分为若干区域
存放class文件的区域、常量池(如final)、静态区(static)等
本地方法区:自己的程序无法直接使用,一般是 jvm 加载被 native 修饰方法所在的区域
寄存器:给cpu使用的
堆空间开辟的空间都会有默认初始化值,栈中却没有。
int a;
a没有值,不能使用;int[] arr = new int[10];
arr中每一个值都为0;
引用变量
有些类似于指针,存放于栈中,内容存放的是别的空间在内存中的位置。(与之相对的,基本类型空间内存的是对应的常量数据)
类加载
程序中,如果需要用到某个类,JVM会从内存种找有没有这个类对象得class文件,如果没有,JVM会将该文件从硬盘加载至内存得方法区中。后续程序还用到该class,JVM不会二次加载,会直接使用内存中得它。
- dos窗口中,输入java指定对应class文件后,按下回车,启动JVM,JVM划分内存
- JVM开始加载对应class文件至方法区中
- 如果存在静态代码块,则执行
- 静态代码块执行结束后,加载流程结束
class Demo{
private static int x = 12;
private int y = 13;
static {
System.out.println(x);
x = 122;
}
{
// 构造代码块,会在构造方法执行之前执行
System.out.println(y);
y = 333;
}
public Demo(int y){
// 隐式三步,如果调用了this,则没有这三步。但实际上是在this方法中执行的
//
System.out.println(this.y);
this.y = y;
}
}
public class DemoTest{
public static void main(String[] args){
Demo d = new Demo(999);
// 执行结果
// 12
// 13
}
}
具体执行过程:
- JVM启动,加载DemoTest.class文件进方法区
- 执行DemoTest中得main方法,main方法进栈
- main方法使用到了Demo.class, JVM会该文件加载至方法区
- 类中如果有静态变量,会先在方法区的静态区给各个静态变量开辟空间,进行默认初始化
- 默认初始化完成后会立刻进行显式初始化
- 所有默认静态变量显式初始化完成后会执行静态代码块
对象创建过程
- 在堆中开辟Demo的空间,并将非静态的成员变量的空间开辟在该对象中
- 所有非静态成员变量空间开辟完成后,JVM会对其进行默认初始化
- JVM调用对应的构造方法至栈中运行,main方法在栈第二层
- 隐式三步:给当前成员变量进行显式初始化;构造代码块执行;构造代码执行
注:如果构造方法调用了this(),那么不会在当前方法中执行隐式三步,但是会在this()方法中执行隐式三步
变态面试题
单例设计模式
设计模式关注要点:1. 解决什么问题。2. 如果实现这个设计模式
单例设计模式解决的问题:保证一个类的对象,在程序内永远只有一个。
如何实现:
饿汉式写法
这么叫的原因是,类一加载,就赶紧去创建对象。开发时常用这个。
class Single{
/**
* 如果一个类的外面可以创建这个类,那么别的程序都可以创建。 也即这个类的对象不可能只有一个
* 如果不让在类的外卖创建该对象,就意味着类以外的程序中不可能有当前类的对象
* 在类以外的地方不让创建对象,可以让这个类的构造方法为私有
*/
// 私有构造方法
private Single(){}
// 在类中创建自己的对象
private static Single instance = new Single();
// 提供get方法, 修饰为static,这样外界才能调用get方法访问。
public static Single getInstance(){
return instance;
}
// 其他部分还照常书写,如成员变量、成员函数
}
懒汉式写法
类加载了后,有需要再去创建对象
class Single{
/**
* 如果一个类的外面可以创建这个类,那么别的程序都可以创建。 也即这个类的对象不可能只有一个
* 如果不让在类的外卖创建该对象,就意味着类以外的程序中不可能有当前类的对象
* 在类以外的地方不让创建对象,可以让这个类的构造方法为私有
*/
// 私有构造方法
private Single(){}
// 只定义变量,不创建对象
private static Single instance = null;
// 提供get方法, 修饰为static,这样外界才能调用get方法访问。
public static Single getInstance(){
if(instance == null){
instance = new Single();
}
return instance;
}
// 其他部分还照常书写,如成员变量、成员函数
}
面试点
面试:为什么写成静态?解:因为类以外的地方无法创建该类对象,如果get方法不静态,外界无法获得这个类的对象。只有get方法静态了,外界才能通过这个方法获得这个类的唯一的对象。
继承
利与弊
利:1. 子类可以使用父类中非私有的内容,无需去创建父类对象。2. 为多态提供可能
弊:打破了封装性,使类与类关系紧密,但一个类的内容被另一个类使用,有可能导致原类的内容暴漏给外界
父类空间
创建子对象的时候,会同时创建父对象的空间,这个空间也会在子对象空间中。但是子对象不能直接访问这个空间。
子类初始化时一定会先调用隐式的super()来找父类的构造方法。坑:super()调用父类构造方法,一定要放在构造方法第一行。注意如果构造方法中先调用了this(),那么这里就不会调用super(),但是调用的this()中,一定会调super()。
注意 如果一个类中没有提供 public 的空参构造,那么必须显式调用 super()
面试题:一个类的所有构造方法均私有,那么它可以有自己的子类吗?
答:不可以,所有构造方法私有,那么如果有子类,该子类的构造方法无法访问到父类的构造方法。所以可以说单例是没有子类的。
super
super区分父类成员变量与子类成员变量同名问题。前提是父类成员变量非私有
final
final可以修饰类、方法(不能是构造方法)、变量(成员变量、局部变量)
final修饰的类:最终类,不能有子类
final修饰的方法:最终方法,子类不能复写(覆盖)
final修饰的变量:是个常量,其中空间的值无法改变
方法复写(覆盖、重写)
- 子类复写父亲方法时,要求返回值类型、方法名、参数列表一致
- 子类可修改方法体
- 子类复写父类方法时,子类方法的权限修饰符可以大于等于父亲
- 父类方法静态,子类若要复写,方法也要静态
- 父类方法私有,子类不存在复写。即使子类中出现了一模一样的方法,也只是相当于一种新方法。
顶层父类
java中若一个类没有手动指定父类,依然有默认父类为Object类,其是最顶层父类
抽象类
抽象类可以写类中都可书写的内容:成员变量、静态代码块、构造代码块、构造方法、普通方法…。唯一的区别是抽象类中可以书写没有方法体的类。
细节
- 抽象类一定是父类,但不是最顶层父类;一般抽象类都有子类,目的是让子类复写抽象类的抽象方法,来完成附和子类自己特点的行为体
注意:如果子类没有复写全部抽象类的抽象方法,那么这个子类也是一个抽象类 - 抽象类不能创建对象,如果可以创建对象,那么意味着可以调用抽象方法,由于抽象方法没有方法体,调用时没有意义的
- 抽象类中有构造方法,虽然抽象类不能创建对象,但它有子类,子类构造函数会调用父类的构造方法。抽象类中的构造方法时为创建子类时进行初始化而提供服务的
- 抽象类可以不写抽象方法。它存在的目的时不让创建这个类的对象,是用于完成适配器设计模式
- 抽象关键字不可以与以下关键字共存:
- final:final修饰的类不能被继承,修饰的方法不能被复写;而abstract修饰的类必须有子类,修饰的方法要求子类必须复写
- static:static修饰的方法可以通过类名直接调用;abstract修饰的方法没有方法体,如果共存,意味着可以通过类名直接调用。
- private:private修饰的方法只能在本类使用;abstract修饰的方法要求子类复写,一旦私有了,根本继承不到。
包
包名一般是域名倒着写。如: .neuedu.项目名.模块名.子功能…
与前端浏览器相关:com.neuedu.web(controller, actions)
处理各种业务逻辑:com.neuedu.service
处理与数据库交互:com.neuedu.dao(cao\mapper)
存放工具类:com.neuedu.utils
异常处理类:com.neuedu.exception
普通的java类:com.neuedu.pojo(domain\beans)
包之间访问
同一个包内的类可以相互访问,如果在不同的包下,需要import进行导入,快捷键ctrl+shift+o
具体而言,修饰符见下:
private default protected public
同一个类 可以访问 可以 可以 可以
同一个包 不可以 可以 可以 可以
不同包的子类 不可以 不可以 可以 可以
不同包不是子类 不可以 不可以 不可以 可以
portected:受保护权限,主要功能是声明当前功能是给子类使用的。
接口
接口:定义一些双方应该遵守的规则
格式
public interface Inter{
}
JDK8之前,接口内只能书写1. 成员变量 2. 抽象方法。且有固定的修饰符: public static final 成员变量; 2. public abstract 抽象方法
使用场景:当子类们共有一些功能,但这些功能放在父类不合适,就可以放在接口之中
接口和类的关系
类和类之间是继承、且是单继承;子父类中称为方法复写
类和接口之间关系是实现,而且可以多实现。类和接口中称为方法实现
接口和接口之间是继承,且可以是多继承。
多个接口有重名
如果一个接口继承多个接口,而多个接口中的有重名变量,那么子类使用时必须指定其属于哪个接口。
JDK8后的接口
接口中的方法
JDK8中允许定义非抽象方法,可以写默认方法与静态方法;JDK9中可以定义私有方法
默认方法:
public default void func(){
System.out.println("接口中默认方法");
}
静态方法:可以通过接口名直接调用
public static void func(){
System.out.println("接口中静态方法");
}
私有方法:由接口类中其他方法来调用
private void func(){
System.out.println("接口中私有方法");
}
接口中的方法细节
- 静态方法只能通过接口来调用
- 实现类的对象可以调用接口的默认方法;如果实现类重写了该方法,那么调的就是重写的方法
- 类优先:子类继承父类定义的同名、同参的默认方法,没有重写时,默认调用的是父类的方法而不是接口中的方法
- 接口冲突:如果实现类实现了多个接口,多个接口均定义同名、同参默认方法,那么如果实现类没有重写则报错,必须要在实现类中重写
- 接口名.super关键字:在实现类中调用多个接口的同名默认方法,需要用接口名.super.默认方法名
适配器设计模式
设计模式关注要点:1. 解决什么问题。2. 如果实现这个设计模式
适配器设计模式解决的问题:从接口过渡到真正的类之间的桥梁。
具体而言:当一个接口中有很多抽象方法时,一个类需要接口但不需要全部方法时。(JDK8前,接口中均为抽象方法)如果类实现了接口,那么类需要复写所有抽象方法,如果不复写/复写部分,那么这个类还是抽象类,那么外界无法直接创建接口的实现类对象,即该类不能用
简单说明:为解决一个接口内有两个以上抽象方法,但实际使用中仅使用部分方法的情况,可以定义一个抽象类,将所有方法空实现。这个类也叫适配器类,真正使用的类再来继承适配器类即可。
如何实现:
interface Inter{
public void demo1();
public void demo2();
public void demo3();
public void demo4();
}
abstract class InterAdapter implements Inter{
// 适配器类/过渡类
public void demo1(){}
public void demo2(){}
public void demo3(){}
public void demo4(){}
}
class InterImpl extends InterAdapter{
// 假设我只想用demo1 与 demo2
public void demo1(){
System.out.println("1");
}
public void demo2(){
System.out.println("2");
}
}
多态
java中:将子类或者实现类的对象采用了父类或接口的类型进行展示。
代码:父类类型/接口类型 引用变量名 = new 子类/实现类()
前提:类必须存在继承或者实现的关系
隐式多态:
Dog d = new Dog();
demo(d);
...
public static void demo(Animal a){
// 这里实际上可以理解为 Animal a = d,也是一种多态
}
多态利弊
利:
- 在庞大体系中,只需要关注顶层父类/顶层接口中的功能,只要顶层能满足程序要求,那么可以使用顶层的引用变量指向底层的对象,调用对应方法即可。降低了学习成本。关注顶层方法定义,不关注底层方法实现或者说多态屏蔽了底层的实现细节
弊: - 多态屏蔽了底层自己特有的内容,只关注了顶层内容。
- 代码中使用多态后,只能访问顶层父类、接口的方法,无法使用子类特有的方法。
多态转型
多态中,是子类或者实现的对象在干活,但表现出的一定是父类或接口的类型。如果想要使用子类的特有方法,需要将其类型进行转型
向上转型
就是多态自己。
向下转型
使用强制类型转换, 由于多态实际上指向的对象还是子类对象,只是引用变量不同罢了
Animal a = new Dog();
Dog dog = (Dog) a;
坑: 多态转型中的异常问题。解决方法:在向下转型时一定要进行判断。
public static void demo(Animal a){
// 假设Animal有两个子类,cat和dog,现在传入了一个dog
// 但是我们想利用cat独有的抓老鼠方法
Cat c = (Cat) a;
c.catchMouse();
// 这里会报类型转换异常
// 新写法, instanceof关键字用于判断类型是否相同
if( a instanceof Cat){
Cat c = (Cat) a;
c.catchMouse();
}
...
}
多态中成员调用问题
成员变量
编译时,要求父类/接口中必须有该变量;运行时,运行的父类或接口中的变量
成员方法
编译时,要求父类/接口中必须有该方法;运行时,运行子类或实现类中的方法
静态方法
实际上静态方法不是按照对象来调的,而是根据类来调的,自然调的时父类的。
SuperClass sc = new SubClass();
sc.demo();// demo是静态方法,调用的是父类的该方法
总结:
成员方法:编译看父类,运行看子类。
变量、静态方法:编译运行均看父类。
内部类
将一个类定义在另一个类的内部。用于在描述事物时,事物内部还有别的事物。如人内部有器官。
内部类可以访问外部类的数据,但是外部类不能访问内部类的数据。
class Outer{
class Inner{
}
static class StaticInner{
public static void demo(){
}
}
}
``
使用
```java
// 没有被私有的成员内部类,在外部类之外的地方访问
Outer out = new Outer();
Outer.Inner in = new Outer().new Inner();
// 访问外部类中静态的非私有的成员内部类
Outer.StaticInner in2 = new Outer.StaticInner();
// 访问外部类的静态的非私有的成员内部类的静态函数
Outer.StaticInner.demo();
坑: 内部类的成员私有,那么内部类也必须私有。
局部内部类
局部内部类仅能使用于局部,出了这个局部就不能使用了。使用场景很少。
class Outer{
public void demo1(){
class StaticInner{
// 局部内部类
public int x = 12;
public static void demo2(){
}
}
StaticInner in = new StaticInner();
...
}
}
匿名内部类
经常用于局部。
interface Inter{
public void demo();
}
class InterImpl implements Inter{
public void demo(){}
}
class Outer{
// 注意这里的参数是Inter这个接口类型,而接口不能new对象,所以传入的只能是接口的实现类,这也是一种多态
public void func(Inter in){
in.demo();
}
}
...
Outer out = new Outer();
InterImpl ii = new InterImpl();
out.func(ii);
...
这段代码主要想使用Outer的func方法,但是这个方法接受的是 Inter 类型, 所以只能定义一个类去实现接口,实现接口方法,然后创建该类的对象,将此对象传入并运行。
针对这一情况,用了很多冤枉代码。实际上只是想用 Outer 的 func 方法而已。
interface Inter{
public void demo();
}
class Outer{
// 注意这里的参数是Inter这个接口类型,而接口不能new对象,所以传入的只能是接口的实现类,这也是一种多态
public void func(Inter in){
in.demo();
}
}
...
Outer out = new Outer();
// 这里就是匿名内部类的使用,创建接口类型的对象,在其中对demo进行实现。
// 匿名内部类既是类也是对象。
out.func( new Inter(){
public void demo(){
System.out.println("");
}
}
);
// lambda 书写
out.func( ()->{ System.out.println("");});
...
lambda
jdk8给了一种匿名内部类的升级写法
格式:(类型 变量名, 类型 变量名) -> {方法体}
异常
编译时期与运行时期
编译时异常:源代码编译时,程序如果扔出exception(除RuntimeException或其子类),这时编译器会强制检测扔出的异常有无处理方案,如果没有,则无法编译通过。
该异常的使用:定义方法时,如果方法有异常扔出,且要求调用这个方法的程序必须在写代码的时候就对异常给出处理方案,如果没有处理方案,就无法编译通过。处理方式:往上扔/try-catch
在对应的方法上需要用throws关键字,声明抛出的编译时异常。运行时异常无需这样做
运行时异常:如果发生这个异常,只会在运行时期产生。编译期间,编译器不予检测。也即如果程序扔出RuntimeException,使用者调用了拥有这些异常的程序,使用者可以不给任何处理方案。但此类异常一旦发生,程序即中止运行。
自定义异常
自定义异常的必要条件:
- 必须继承某个已经存在的异常类,否则它就是一个普通的类
- 必须提供最少两个公开的构造方法,一个无参,一个有参可以接受一个字符串。函数体内第一行得是
super(msg)
, msg为传入字符串
细节
- 子父类中,如果子类需要复写父类方法,父类方法上没有throws关键字声明任何异常,子类也不能声明异常
- 子父类中,如果子类需要复写父类方法,父类方法上声明了异常,子类复写时可以不声明异常,或者声明和父类相同的异常,或者声明父类已经声明的异常的子异常,或者子类只声明部分异常
- 程序中出现throw关键字抛出异常,则这行代码下方不能有别的代码,因为它不会被执行。
- 程序中有代码不管有无代码均要执行,那么可以使用 try{}finally{},放在finally中的代码无论如何一定执行。
字符串
字符串缓冲区
字符串本身是不可变的数据,一旦书写,即在常量池种固定死,改变字符串的某个字符只会生成新的字符串。
字符串缓冲区,目的是提供可以对字符串中的字符数据进行各种操作的临时区域,在这个临时区域中操作字符串中的字符不会在常量池中生成新的字符串,最终等所有操作结束后,缓冲区将统一将操作的数据转成一个字符串放入常量池。
注:只要是容器、缓冲区,一般会提供四种固定操作:增删改查。
这两个缓冲区即:StringBuffer, StringBuilder。两者几乎相同,区别在于Buffer是线程安全的,Builder是线程不安全的
创建缓冲区:底层是创建了一个16空间的byte数组(JDK9前是char数组)
反转:.reverse()方法。String本身不可变,所以其反转方法是在缓冲区的方法中的。
容积、长度:capacity(), length()