Java基础知识复习(二)

Java基础知识复习(二)

2、Java面向对象

2.1 面向对象和面向过程的区别

  • 面向过程:面向过程性能比面向对象高。因为类的调用需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素时,如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。
  • 面向对象:面向对象易维护、易复用、易扩展。所以面向对象比面向过程更加灵活或、更加容易维护。但是性能没有面向过程高。

2.2 构造器

2.1.1. 构造器Constructor是否可被override

Constructor不可以被override,但是可以被overload,所以可以在一个类中看到多个构造方法的情况。

2.1.2. 为什么要在一个类中定义一个不做事的无参构造方法

一个类在被初始化时,如果没有用super()来指定调用父类的某个构造方法(如果没有父类,默认是Object),则会调用父类中的无参构造方法(默认有一个隐式的super(),所谓隐式就是看不见)。所以,如果父类中只定义了有参构造方法,没有定义无参构造方法,此时就会发生编译错误。因为Java程序在父类中找不到可执行的无参构造方法。解决办法是在父类定义一个无参构造方法。

2.1.3. 一个类的构造方法的作用是什么。若没有在类中定义构造方法,对象是否能够被正确创建

作用是完成对类对象的初始化工作。若没有在类中定义构造方法,也会默认有一个隐式的无参构造方法。但如果我们自己写了构造方法(无论是否有参),Java就不会默认为我们添加无参的构造方法了。而且,如果我们只定义了有参构造方法,那么在创建对象的时候必须要传递参数了。所以我们创建一个类的时候,最好手动的把无参构造方法写出来,不管它是否有用。

2.1.4. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

帮助子类做初始化工作。

2.2 面向对象的特性

这里这里之前写过了,就不再赘述。

2.3 抽象类和接口

2.3.1. 接口和抽象类的区别

  • 接口需要被实现,抽象类需要被继承
  • 一个类允许实现多个接口,但只允许继承一个抽象父类
  • 接口中的方法都是默认公共抽象的,抽象类中则既允许定义抽象方法也允许普通方法(jdk8中,接口被允许定义默认方法,jdk9中还允许定义私有方法)。
  • 接口是对类的规范,规范的是行为能力。抽象类是对类的抽象,抽象的是逻辑。

2.4 内部类

2.4.1 内部类的特点

  • 内部类方法可以访问该类定义治所在作用域中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 每一个内部类都能继承或实现类或接口,无论外部类是否已经继承过这个类或接口。

2.4.2 为什么内部类能够直接使用外部类的变量?

创建一个带有内部类的java程序

public class OutterClass {
    private InnerClass inner = null;
    public OutterClass() {

    }

    public InnerClass getInnerInstance() {
        if(inner == null)
            inner = new InnerClass();
        return inner;
    }
    public class InnerClass{
        public InnerClass() {
        }
    }

    public static void main(String[] args) {
       OutterClass.InnerClass outterClass = new OutterClass().getInnerInstance();

    }
}

编译带有内部类的java文件,会得到两个.class文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f7SmmIrg-1592146816644)(G:\java文档\image\内部类.png)]

反编译 OutterClass$InnerClass.class :

public class generics.test.OutterClass$InnerClass {
  final generics.test.OutterClass this$0;

  public generics.test.OutterClass$InnerClass(generics.test.OutterClass); 
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Lgenerics/test/OutterClass;
       5: aload_0
       6: invokespecial #2                  // Method java/lang/Object."<init>":()V
       9: return
}

可以看到,虽然我们没有声明有参构造方法,但是,Java还是给给我们默认传递了一个外部类的实例,这个外部类实例就是用来调用外部类的数据的。

2.4.3 成员内部类中为什么不能定义静态变量和静态方法?

1)、如果内部类拥有静态数据,但是这个内部类并不是静态内部类,那么jvm在加载外部类的时候就不会加载这个内部类。

2)、static类型的属性和方法,在类加载的时候就会存在于内存中。

3)、要是用某个类的静态变量或者方法,这个类必须先存在于jvm内存中。

2.4.4 什么是局部内部类,能不能被外部类访问?

局部内部类是定义在方法中,不能用publicprivate关键字声明的内部类,它的作用域被限定在声明这个局部内部类的方法体中,所以它并不能被外部类所访问。

2.5. 修饰符

2.5.1. 在一个静态方法内调用一个非静态成员为什么是非法的?

由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。

即不能调用和访问一个不存在的变量。

2.5.2. 静态方法和实例方法有何不同

  1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。

2.6 其他

2.6.1 String StringBuffer和StringBuilder的区别是什么?String为什么是不可变的?

String类中使用final关键字修饰字符数组来保存字符串(在Java9之后,使用byte数组存储字符串)

StringBuilderStringBuffer都继承自AbstractStringBuilder类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的(Java9后也是变成byte数组)。

StringBuilderStringBuffer中的所有方法实现基本上都是直接调用父类方法。

StringBuffer中的部分方法:

@Override
public synchronized char charAt(int index) {
    return super.charAt(index);
}

/**
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @since      1.5
 */
@Override
public synchronized int codePointAt(int index) {
    return super.codePointAt(index);
}

/**
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @since     1.5
 */
@Override
public synchronized int codePointBefore(int index) {
    return super.codePointBefore(index);
}

线程安全

String中的对象都是不可变的,可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作,如insert,append等,Stirngbuffer对重写的方法都加上了同步锁或对调用的方法加上了同步锁,所以是线程安全的。StringBuilder并没有对方法进行枷锁,所以它是非线程安全的。

性能

String由于是不可变的,每次操作都需要重新创建一个对象,所以效率比较低,StringBufferStringBuilder都是可变的字符串,但由于StringBuffer是线程安全的,所以在单线程环境下,效率会比StringBuilder稍微低点。

三者的使用:

操作少量数据,可以考虑用Stirng

在单线程环境下,使用StringBuilder

在多线程环境下,保证线程安全,使用StringBuffer

2.6.2 Object类的常见方法

 public final native Class<?> getClass();
 public native int hashCode();
 public boolean equals(Object obj)
 protected native Object clone() throws CloneNotSupportedException;
 public String toString()
 public final native void notify()
 public final native void notifyAll()
 public final void wait()
 public final native void wait(long timeoutMillis) throws InterruptedException; //让当前对象进入TIMED_WATING状态
 public final void wait(long timeoutMillis, int nanos) throws InterruptedException //让当前对象进入TIMED_WATING状态
 protected void finalize() throws Throwable

==和equals的区别

==:它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是属于同一个对象(引用同一块内存地址)。(基本数据类==比较的是值,引用数据类型比较的是内存地址)

Java中只有值传递,所以,对于==来说,不管是比较基本数据类,或者是引用数据类型,其本质都是比较值,只是引用数据类型的值是对象的内存地址。

equals():它的作用是判断两个对象是否相等,他不能用于比较基本数据类型。equals()方法存在于ObjectObject是所有类的直接或间接父类。

Objectequals()

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

从上面可以看出,如果需要使用equals()来比较两个对象是否相等,需要重写equals()

举个例子:

public class Test1 {
    public static void main(String[] args) {
        String a =new String("ab"); // 创建了两个对象,一个字符串"ab"对象,一个String对象
        String b = new String("ab");// 同理
        String aa = "ab"; // 创建了一个字符串"ab"放在常量池中
        String bb = "ab"; // 从常量池中查找,否则自己创建一个新的
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一对象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}

说明:

  • String类已经重写了equals(),因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。

String的equals()

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}

2.6.3 hashcode()与equals()

1. hashcode()

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 方法。hashcode方法一个Java本地方法,也就是说,这个方法可能是由c或c++语言实现的,用来将对象的地址计算后返回。

但是,只有当创建某个类的散列表时,hashcode()才有作用,其他情况下并没有作用。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。(可以快速找到所需要的对象)

为什么重写 equals 时必须重写 hashCode 方法?

如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

为什么两个对象有一样的hashcode值,但两个对象不一定相等

因为hashcode()使用的算法可能会使不同的对象,恰巧计算出相等hashcode值,当两个对象hashcode值一样时,散列表会调用对象的equals()来比较两个对象的内容是否相等。也就是说hashcode只是用来缩小查找成本。

重写equals() 的规范

1)、自反性:对于任何非空引用x,x.equals(x)应该返回true。

2)、对称性:对于任何引用x和y。只有当y.equals(x)返回true,x.equals(y)也应该返回true。

3)、传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)也应该返回true。

4)、一致性:如果x和y引用的对象没有发送变化,返回调用x.equals(y)应该返回同样的结果。

5)、对于任意非空引用x,x.equals(null)应该返回false。

2.6.4 Java序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用transient关键字修饰。

transient关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

2.6.5 自动拆箱和自动装箱

自动拆箱和自动装箱时Java编译器的一个语法糖。

自动装箱是指:将基本数据类型转为对应的包装类对象的过程。

自动拆箱是指:将包装类对象转为对应基本数据类型的过程。

自动装箱实际上调用了包装类对象的一个方法:valueof()

自动拆箱实际上调用了包装类对象的一个方法:intvalue()

在自动装箱时,处于节约内存的考虑,JVM会缓存处于缓存值范围内的对象。

Integer,Byte,Short,Long,Character包装类型具有缓存池, 而其他三种:Float,Double,Boolean不具有缓存池。

包装类的缓存池范围都在 -127~127之间,除了Character 在 0~127之间。

3 Java在内存中的布局

一个Java对象在内存中分为3个部分

  • 对象头
  • 实例数据
  • 对齐填充

其中实例数据和对齐填充是不固定的。

3.1.对象头

对象头是jvm在每次GC时管理的对象的通用结构,包含了对象的布局类型(Class Type),GC状态同步状态hashcode等信息,在数组对象中,还会跟随数据的长度。Java对象和vm对象都具有通用的对象头格式。

对象头主要由两部分组成

  • Mark Word
  • 类型指针
  • 数组长度(如果是数组才有)

3.1.1 Mark Word

Mark Word 的定义:

Mark Word 用于存储对象自身的运行时数据,如哈希码(hashcode)、GC 状态、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据的长度在32位和64位的虚拟机中分别为32 bit 和 64 bit。

问题:如果对象需要存储的运行时数据很多,超过了所在操作系统Bitmap 结构所能记录的限度应该怎么办? (对象头是与对象本身无关的额外存储成本)

考虑到虚拟机的空间效率,Mark Word 被设计成一个非固定的数据结构,以便在极小的空间内尽可能存储更多的信息,它会根据对象的状态复用自己的存储空间。

Mark Word组成

锁状态 锁标志 markword组成
无锁 01 由hashcode,分代年龄,是否偏向锁(1位),锁标志位组成
偏向锁 01 由偏向线程的ID,偏向时间戳(epoch),是否偏向锁
轻量级锁 00 由指向栈中锁的记录和锁标志位组成
膨胀锁 10 由指向锁的指针和锁标志位组成
GC 11 无数据

3.1.2 类型指针

类型指针即对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

3.1.3 数组长度

如果对象是一个Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java 对象的元数据确定Java 对象的大小,但是从数组的元数据无法确定数组的大小

3.1.4 实例数据

实例数据存储着对象在程序中被定义的各个字段的数据,也就是对象的字段数据。如果一个类没有字段,那么也就不存在实例数据,所以实例数据并不固定。

3.1.5 对齐填充

Java对象的大小必须是8字节的倍数,像13,15这种非8的倍数对象的大小,不足或多余的部分就要使用对齐填充数据补齐。如果Java对象大小正好是8的倍数,那么就无需填充数据。

4 Java多线程

进程和线程

什么是进程,什么是线程

把进程比喻为火车,把线程比喻为火车车厢

  • 线程只能在某一个进程下进行(单独一个车厢无法运行)
  • 一个进程可以有多个线程(一列火车可以有多节车厢)
  • 进程之间,数据共享复杂(A火车上的乘客很难换到另外一辆火车上)
  • 线程之间,数据共享简单(A车厢的乘客可以随意走动到其他车厢)
  • 进程之间不会互相影响,但是一个线程崩溃会导致整个进程崩溃(不同的火车之间是独立的,但是一辆货车上的车厢着火了,整辆火车都不能行驶)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”

并发和并行

1、并发:多个线程任务被一个或多个cpu轮流执行。并发强调的是计算机应用程序有处理多个任务的能力。

2、并行:多个线程被一个或多个cpu执行。并行强调的是计算机应用程序拥有同时处理多任务的能力。

多线程的利弊

利:

  • 线程可以比作轻量化的进程,cpu在线程间的切换和调度的成本远远小于进程。
  • 现在是多核CPU时代,意味着多个线程可以同时进行,那么如果我们可以利用好线程,就可以编写出高并发的程序。

弊:

  • 虽然多线程带来的好处很多,但是想要并发编程并不容易 ,如果不能很好的控制线程,那么就可能造成死锁,资源闲置,内存泄漏等问题。

什么是上下文切换

上下文切换(有时候也被称作进程切换或任务切换)是指CPU从一个进程(或线程)切换到另一个进程(或线程)的操作。上下文指的是某一个时间点CPU寄存器和程序计数器的内容

寄存器是CPU内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。

程序计数器是一个专用的寄存器,用于表明指令序列中的CPU正在执行的位置。

上下文切换前,会先保存当前线程的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。保存线程的状态再到重新加载回线程的状态的这个过程就叫做上下文切换。

线程的优先级

再Java中可以通过Thread类的setPriority方法来设置线程的优先级,虽然通过这样的方式可以设置线程的优先级,但是线程执行的先后顺序并不是依赖优先级决定的,换句话说,线程的优先级不能保证线程的执行顺序。

线程的状态

线程可以有如下6种状态:

  • New(新创建的线程)
  • Runnable(可运行的线程)
  • Blocked(被阻塞的线程)
  • Waiting(等待中的线程)
  • Timed waiting(在一个限定时间内等待的线程)
  • Terminated(终止的线程)

Blocked、Waiting、Timed waiting 的区别

Blocked


Java文档官方定义BLOCKED状态是:“这种状态是指一个阻塞线程在等待monitor锁。”

比如有一个方法,这个方法被加上了synchronized关键字,现在有两个线程,一个线程B获取到synchronized锁,然后在执行方法。但是呢,线程A也想执行这个方法,但是由于获取不到锁,只能等待,并尝试获取monitor锁。

Waiting


Java文档官方定义WAITING状态是:“一个线程在等待另一个线程执行一个动作时在这个状态”

在某一个对象线程上调用的Object.wait( ),这个线程就会进入Waiting状态,当有另外一个线程在这个对象上调用Object.notify()或者调用了Object.notifyAll(),这个线程才会恢复。一个调用了Thread.join()的线程也会进入WAITING状态直到一个特定的线程来结束。

Timed waiting


Java文档官方定义TIMED_WAITING状态为:“一个线程在一个特定的等待时间内等待另一个线程完成一个动作会在这个状态”

比如你在开车,经过一个红绿灯路口,现在红绿灯的状态是红灯,所以你需要停下并等待红灯变为绿灯,你才可以继续行驶。

调用了以下方法的线程会进入TIMED_WAITING

  1. Thread.sleep()
  2. Object.wait()并加了超时参数
  3. Thread.join()并加了超时参数
  4. LockSupport.parkNanos()
  5. LockSupport.parkUntil()

Sleep方法和wait方法的区别

从方法调用上:

Sleep方法是Thread类的方法,wait方法是Object类的方法。

从功能上:

  • 调用Sleep方法会使当前线程让出cpu的调度资源,使其他线程获得被执行的机会。但是Sleep方法不会让当前线程释放锁
  • 调用wait方法会使当前线程直接进入waiting状态并且释放锁,不参与锁的竞争,使其他等待资源的线程有机会获得锁,等待其他线对其调用notify()或者调用notifyAll(),才会重新与其他线程争夺资源。

stop,suspend,resume等方法为什么会被遗弃

  • stop:stop方法的作用是强行终止线程的执行,不管线程的run方法是否正在执行,资源是否已经释放,它都会终止线程的运行,并释放所有资源(包括锁)。所以这个方法并不是很合理。
  • suspendresume:suspend方法用于阻塞一个线程,但并不释放锁, 而resume方法的作用只是为了恢复被suspend的线程。 假设A,B线程都争抢同一把锁,A线程成功的获得了锁, 然后被suspend阻塞了,却并没有释放锁,它需要其他线程来唤醒, 但此时B线程需要获得这把锁才能唤醒A,所以此时就陷入了死锁。

interrupt,interrupted,isInterrupted方法区别

  • interrupt:这个方法是不中断线程,但是给当前线程设置一个中断状态
  • isInterupted:当线程调用interrupt方法后,线程就有了一个中断状态,而使用isInterupted方法就可以检测线程的中断状态
  • interrupted: 这个方法用于清除interrupt方法设置的中断状态。 如果一个线程之前调用了interrupt方法设置了中断状态, 那么interrupted方法就可以清除这个中断状态。

join方法

join方法的作用是让指定线程加入到当前线程中执行。

下面这段代码,t线程会在main方法执行前执行。

  public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            try
            {
                TimeUnit.SECONDS.sleep(1);
            }catch (Exception ignored){}
            System.out.println("thread join");
        });
        t.start();
        t.join();

        System.out.println("main");
    }

join方法的底层是wait方法,调用A线程(子线程)的join方法实际上是让main线程wait, 等A线程执行完后,才能继续执行后面的代码。

yield方法

yield属于Thread的静态方法, 它的作用是让当前线程让出cpu调度资源。

yield方法其实就和线程的优先级一样,你虽然指定了, 但是最后的结果不由得你说了算, 即使调用了yield方法,最后仍然可能是这个线程先执行, 只不过说别的线程可能先执行的机会稍大一些。

猜你喜欢

转载自blog.csdn.net/WXZCYQ/article/details/106753575