JAVA面试题(基础)

更多文章:TT的博客

1. 面向对象和面向过程的区别

面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发

面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。

2. Java 有哪些基本数据类型?

Java 是一门强类型语言,对于程序中的每一个变量都明确定义了一种数据类型,以及分配不同大小的内存空间

其中,Java 提供了八种基本数据类型格式,包括 4 种整型,2 种浮点,1 种字符(char 是两字节)以及 1 种布尔型类型。

数据类型大小(字节)位数默认值取值范围封装类
byte180-128~127Byte
short2160-32768 ~ 32767Short
int4320-2^31 ~ 2^31-1Integer
long8640L-2^63 ~ 2^63-1Long
float4320f-2^63 ~ 2^63-1Float
double8640d-2^63 ~ 2^63-1Double
char216\u0000(null)0 ~ 2^16-1Character
boolean--falsefalse、trueBoolean
  1. 除了上述所说的八种基本类型外,Java 还提供了一种引用数据类型的支持,比如我们常说的类(class)、接口(interface)和数组。

  2. int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。

  3. 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。

  4. 虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位,计算机处理数据的最小单位是字节。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的是32/64位系统,而是指CPU硬件层面),因此使用一个字节具有高效存取的特点。

3. 类型有哪些转换方式?

对于基本类型而言,主要分为自动(隐式)类型转换和强制(显式)类型转换两种方式。

自动类型转换:是一种小类型到大类型的转换,不需要强制转换符。

image.png

  1. 小的类型自动转化为大的类型。
  2. 整数类型可以自动转化为浮点类型,可能会产生舍入误差。
  3. 字符可以自动提升为整数。
强制类型转换:需要在强制类型转换的变量前面加上括号,然后在括号里面标注要转换的类型。
  1. 强制类型转换可能导致溢出或损失精度。
  2. 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入。
  3. 不能对布尔值进行转换。
  4. 不能把对象类型转换为不相干的类型。

4. Java自动装箱与拆箱

为什么会出现自动拆装箱

Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。使得基本数据类型也具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

既然有了基本类型以及对应的包装类,那么必然会出现二者之间的转换操作,于是在Java SE5中,为了减少开发人员的工作,Java 提供了自动拆箱与自动装箱功能。

自动拆装箱的原理
  1. 自动装箱:自动将基本数据类型转换为包装器类型(int–>Integer)。调用方法:Integer的valueOf()方法。
  2. 自动拆箱:自动将包装器类型转换为基本数据类型(Integer–>int)。调用方法:Integer的intValue()方法。
自动拆装箱的使用场景
  1. 基本数据类型放入集合类。
  2. 包装类型和基本类型的大小比较。
  3. 包装类型的运算符计算。
  4. 方法参数与返回值。

5. 基本数据类型的缓冲池

以下代码会输出什么?

public class Main {
	public static void main(String[] args) {
		Integer i1 = 100;
		Integer i2 = 100;
		Integer i3 = 200;
		Integer i4 = 200;
		System.out.println(i1==i2);
		System.out.println(i3==i4);
	}
}

运行结果:

true
false

为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

其中IntegerCache类的实现为:

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。

上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。

Java 为了进一步提高性能和节省空间,整型 Integer 对象通过使用相同的对象引用实现了缓存和重用。

即当需要进行自动装箱时,在 -128 至 127 之间的整型数字会直接使用缓存中的对象,而不是重新创建一个新对象。当然,这个范围是可以通过-XX:AutoBoxCacheMax=size参数进行调整的。

6. 成员变量、类变量、局部变量

成员变量和局部变量

成员变量:

  1. 成员变量定义在类中,在整个类中都可以被访问。
  2. 成员变量随着对象的建立而建立,随着对象的消失而消失,存在于对象所在的堆内存中。
  3. 成员变量有默认初始化值。

局部变量:

  1. 局部变量只定义在局部范围内,如:方法内,语句内等,只在所属的区域有效。
  2. 局部变量存在于栈内存中,作用的范围结束,变量空间会自动释放。
  3. 局部变量没有默认初始化值

在使用变量时需要遵循的原则为:就近原则。首先在局部范围找,有就使用;接着在成员位置找。

如果局部变量的名字和成员变量的名字相同, 要想在该方法中使用成员变量,必须使用关键字this。

成员变量(实例变量)和类变量(静态变量)

由static修饰的变量称为类变量(静态变量),其实质上就是一个全局变量。如果某个内容是被所有对象所共享,那么该内容就应该用静态修饰;没有被静态修饰的内容,其实是属于对象的特殊描述。

不同的对象的实例变量(成员变量)将被分配不同的内存空间, 如果类中的成员变量有类变量,那么所有对象的这个类变量都分配给相同的一处内存,改变其中一个对象的这个类变量会影响其他对象的这个类变量,也就是说对象共享类变量。

即:

  1. 想要实现对象中的共性数据的对象共享。可以将这个数据进行静态修饰。
  2. 被静态修饰的成员,可以直接被类名所调用。也就是说,静态的成员多了一种调用方式。类名.静态方式。
  3. 静态随着类的加载而加载。而且优先于对象存在。

弊端:

  1. 有些数据是对象特有的数据,是不可以被静态修饰的。因为那样的话,特有数据会变成对象的共享数据。这样对事物的描述就出了问题。所以,在定义静态时,必须要明确,这个数据是否是被对象所共享的。
  2. 静态方法只能访问静态成员,不可以访问非静态成员。因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。
  3. 静态方法中不能使用this,super关键字。因为this代表对象,而静态在时,有可能没有对象,所以this无法使用。

什么时候定义静态成员呢?或者说:定义成员时,到底需不需要被静态修饰呢?

  1. 成员变量(数据共享时静态化)
    该成员变量的数据是否是所有对象都一样:
    如果是,那么该变量需要被静态修饰,因为是共享的数据;
    如果不是,那么就说这是对象的特有数据,要存储到对象中。
  2. 成员函数(方法中没有调用特有数据时就定义成静态)
    判断成员函数是否需要被静态修饰,只要参考,该函数内是否访问了对象中的特有数据:
    如果有访问特有数据,那方法不能被静态修饰。
    如果没有访问过特有数据,那么这个方法需要被静态修饰。

成员变量和类变量的区别:

  1. 两个变量的生命周期不同
    成员变量随着对象的创建而存在,随着对象的回收而释放。
    静态变量随着类的加载而存在,随着类的消失而消失。
  2. 调用方式不同
    成员变量只能被对象调用。
    静态变量可以被对象调用,还可以被类名调用。
  3. 别名不同
    成员变量也称为实例变量。
    静态变量也称为类变量。
  4. 数据存储位置不同
    成员变量存储在堆内存的对象中,所以也叫对象的特有数据。
    静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据。

7. 抽象类和接口

抽象类:含有abstract修饰符的class即为抽象类,abstract 类不能创建实例对象。含有abstract方法的类必须定义为abstract class,abstract class类中的方法不必是抽象的。abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果子类没有实现抽象父类中的所有抽象方法,那么子类也必须定义为abstract类型。

接口(interface):可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final

语法区别
  1. 抽象类可以有构造方法,接口中不能有构造方法。
  2. 抽象类中可以有普通成员变量,接口中没有普通成员变量
  3. 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
  4. 抽象类中的抽象方法的访问类型可以是public,protected和默认类型,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
  5. 抽象类中可以包含静态方法,接口中不能包含静态方法(1.8后可以包含)
  6. 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
  7. 一个类可以实现多个接口,但只能继承一个抽象类。
  8. 使用 extends 继承抽象类并实现抽象方法,是 implements 实现接口中所有方法。
  9. 抽象类可以有 main 方法,接口不支持。
应用区别

接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用,例如,模板方法设计模式是抽象类的一个典型应用,假设某个项目的所有Servlet类都要用相同的方式进行权限判断、记录访问日志和处理异常,那么就可以定义一个抽象的基类,让所有的Servlet都继承这个抽象基类,在抽象基类的service方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中只是完成各自的业务逻辑代码,伪代码如下:

public abstract class BaseServlet extends HttpServlet{
	public final void service(HttpServletRequest request, HttpServletResponse response) throws IOExcetion,ServletException{
		记录访问日志
		进行权限判断
		if(具有权限){
			try{
				doService(request,response);
			}catch(Excetpion e){
				记录异常信息
			}
		}
	}
	protected abstract void doService(HttpServletRequest request,HttpServletResponse response) 	throws IOExcetion,ServletException;
		//注意访问权限定义成protected,显得既专业,又严谨,因为它是专门给子类用的
}

public class MyServlet1 extends BaseServlet{
	protected void doService(HttpServletRequest request, HttpServletResponse response) throws IOExcetion,ServletException{Servlet只处理的具体业务逻辑代码
	}
}

父类方法中间的某段代码不确定,留给子类干,就用模板方法设计模式。

接口的设计目的,是对类的行为进行约束(更准确的说是一种"有"约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。

而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。

抽象类是对类本质的抽象,表达的是is a的关系,比如: BMW is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。

而接口是对行为的抽象,表达的是like a的关系。比如: Bird like a Aircraft(像飞行器一样可以飞),但其本质上is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。

使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。

抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度。

8. 内部类

在 Java 中,内部类就是将自身类的定义放在另外一个类的定义内部的类。内部类本身就是类的一个属性,与其他属性定义方式一致。

常见的内部类可以被分为四种:成员内部类、局部内部类、匿名内部类和静态内部类

  1. 成员内部类:定义在类内部,成员位置上的非静态类。
    成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式是 外部类实例.new 内部类()。
  2. 静态内部类:定义在类内部的静态类。
    静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量。静态内部类的创建方式:new 外部类.静态内部类()。
  3. 局部内部类:定义在方法中的内部类。
    定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,new 内部类(),且仅能在对应方法内使用。
  4. 匿名内部类:就是没有名字的一种嵌套类。它是Java对类的定义方式之一。
    在实际开发中,我们常常遇到这样的情况:一个接口/类的方法的某个实现方式在程序中只会执行一次,但为了使用它,我们需要创建它的实现类/子类去实现/重写。此时可以使用匿名内部类的方式,可以无需创建新的类,减少代码冗余。
@Test
void test2(){
    int d = 4;
    Outside outside = new Outside(){
        final int x = d;
        @Override
        public void show() {
            System.out.println(x + "我是匿名内部类");
        }
    };
    Outside.Inside inside = outside.new Inside();
    inside.show();
    Outside.StaInside staInside = new Outside.StaInside();
    staInside.show();
    outside.FunOutside();
    outside.show();
}

abstract class Outside{
    int a = 1;
    static int b = 2;
    int c = 3;
    class Inside{
        void show(){
            System.out.println(a + "我是成员内部类");
        }
    }
    static class StaInside{
        void show(){
            System.out.println(b + "我是静态内部类");
        }
    }
    void FunOutside(){
        class FunInside{
            final int x = c;
            void show(){
                System.out.println(x + "我是局部内部类");
            }
        }
        FunInside funInside = new FunInside();
        funInside.show();
    }
    abstract void show();
}
局部内部类和匿名内部类访问局部变量时为什么须要加 final?
class Outer {
    void outMethod(){
        int a =10;
        class Inner {
            final int b = a;
            void innerMethod(){
                System.out.println(b);
            }
        }
        Inner inner = new Inner();
        inner.innerMethod();
    }
}

因为生命周期不一致,局部变量直接存储在栈中,当方法执行结束后,非 final 的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。使用 final 后可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。

内部类的优点
  1. 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据。
  2. 内部类不为同一包的其他类所见,具有很好的封装性。
  3. 内部类有效实现了"多重继承",优化 java 单继承的缺陷。
  4. 匿名内部类可以很方便的定义回调。

9. 修饰符

Java语言提供了很多修饰符,大概分为两类:访问权限修饰符、非访问权限修饰符。

同时根据使用对象的不同,可以分为三类:类修饰符、方法修饰符、变量修饰符。

一、访问权限修饰符
  1. public:共有访问。对所有的类都可见。
  2. protected:保护型访问。对同一个包可见,对不同的包的子类可见。
  3. default:默认访问权限。只对同一个包可见,注意对不同的包的子类不可见。
  4. private:私有访问。只对同一个类可见,其余都不见。

访问范围:public > protected > default > private。

修饰符同类同包子类其他包
public
protected×
default××
private×××
二、非访问权限修饰符
  1. static:用来创建类方法和类变量。
  2. final:用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
  3. abstract:用来创建抽象类和抽象方法。
  4. synchronized:用于多线程的同步。
  5. volatile:修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
  6. transient:序列化的对象包含被 transient 修饰的实例变量时,java 虚拟机(JVM)跳过该特定的变量。
三、类修饰符
外部类修饰符
  1. public(访问控制符),将一个类声明为公共类,它可以被任何对象访问,一个程序的主类必须是公共类。
  2. default(访问控制符),类只对包内可见,包外不可见。
  3. abstract(非访问控制符),将一个类声明为抽象类,抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充,抽象类可以包含抽象方法和非抽象方法。
  4. final(非访问控制符),将一个类生命为最终(即非继承类),表示它不能被其他类继承。

注意:
(1)protected 和 private 不能修饰外部类,是因为外部类放在包中,只有两种可能,包可见和包不可见。
(2)final 和 abstract不能同时修饰外部类,因为该类要么能被继承要么不能被继承,二者只能选其一。
(3)不能用static修饰,因为类加载后才会加载静态成员变量。所以不能用static修饰类和接口,因为类还没加载,无法使用static关键字。

内部类修饰符

内部类与成员变量地位一致,所以可以public、protected、default和private,同时还可以用static修饰,表示嵌套内部类,不用实例化外部类,即可调用。

四、方法修饰符
  1. public(公共控制符),包外包内都可以调用该方法。
  2. protected(保护访问控制符)指定该方法可以被它的类和子类进行访问。
  3. default(默认权限),指定该方法只对同包可见,对不同包(含不同包的子类)不可见。
  4. private(私有控制符)指定此方法只能有自己类等方法访问,其他的类不能访问(包括子类),非常严格的控制。
  5. final,指定方法已完备,不能再进行继承扩充。
  6. static,指定不需要实例化就可以激活的一个方法,即在内存中只有一份,通过类名即可调用。
  7. synchronize,同步修饰符,在多个线程中,该修饰符用于在运行前,对它所属的方法加锁,以防止其他线程的访问,运行结束后解锁。
  8. native,本地修饰符。指定此方法的方法体是用其他语言在程序外部编写的。
  9. abstract,抽象方法是一种没有任何实现的方法,该方法的的具体实现由子类提供。抽象方法不能被声明成 final 和 static。任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类。如果一个类包含若干个抽象方法,那么该类必须声明为抽象类。抽象类可以不包含抽象方法。 抽象方法的声明以分号结尾,例如:public abstract sample();
五、变量修饰符
成员变量修饰符
  1. public(公共访问控制符),指定该变量为公共的,它可以被任何对象的方法访问。
  2. protected(保护访问控制符)指定该变量可以别被自己的类和子类访问。在子类中可以覆盖此变量。
  3. default(默认权限),指定该变量只对同包可见,对不同包(含不同包的子类)不可见。
  4. private(私有访问控制符)指定该变量只允许自己的类的方法访问,其他任何类(包括子类)中的方法均不能访问。
  5. final,最终修饰符,指定此变量的值不能变。
  6. static(静态修饰符)指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类。
  7. transient(过度修饰符)指定该变量是系统保留,暂无特别作用的临时性变量。不持久化。
  8. volatile(易失修饰符)指定该变量可以同时被几个线程控制和修改,保证两个不同的线程总是看到某个成员变量的同一个值。

final 和 static 经常一起使用来创建常量。

局部变量修饰符

只能使用 final 修饰局部变量

  1. 为什么不能赋予权限修饰符?
    因为局部变量的生命周期为一个方法的调用期间,所以没必要为其设置权限访问字段,既然你都能访问到这个方法,所以就没必要再为其方法内变量赋予访问权限,因为该变量在方法调用期间已经被加载进了虚拟机栈,换句话说就是肯定能被当前线程访问到,所以设置没意义。
  2. 为什么不能用static修饰
    我们都知道静态变量在方法之前先加载的,所以如果在方法内设置静态变量,可想而知,方法都没加载,你能加载成功方法内的静态变量?

接口
1、接口修饰符
接口修饰符只能用public、default和abstract,不能用final、static修饰。
接口默认修饰为abstract。
2、接口中方法修饰符
只能用 public abstract修饰,当然如果你什么都不写,默认就是public abstract。
注意:在Java1.8之后,接口允许定义static 静态方法了!所以也可以用static来修饰!

10. Java 的四种引用类型

JDK 1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。于是 Java 对引用的概念进行了扩充,将引用分为了:强引用、软引用、弱引用、虚引用,这 4 种引用的强度依次减弱。

  1. 强引用(Strong Reference)
    是 Java 中默认声明的引用类型,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时, JVM 也会直接抛出 OutOfMemoryError 而不会去回收。
String str = new String("str");
  1. 软引用(Soft Reference)
    软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
  1. 弱引用(Weak Reference)
    弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。 使用 java.lang.ref.WeakReference 来表示弱引用。
WeakReference<String> wrf = new WeakReference<String>(str);
  1. 虚引用(Phantom Reference)
    虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,用 PhantomReference 类来表示。通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
PhantomReference<String> prf = new PhantomReference<String>(new
String("str"), new ReferenceQueue<>());

引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

11. 类初始化顺序

  1. 父类–静态变量/静态初始化块(按代码顺序)。
  2. 子类–静态变量/静态初始化块。
  3. 父类–变量/初始化块。
  4. 父类–构造器。
  5. 子类–变量/初始化块。
  6. 子类–构造器。

12. Java 中的取整方法

  1. 强制类型转换。
  2. Math 取整函数。
    Math.ceil(double num):向上。
    Math.floor(double num):向下。
    Math.round(double num):四舍六入,五取正。
  3. BigDecimal#setScale 函数。
  4. String#format 方法。

13. Switch

Switch 支持的数据类型

一般情况下使用整型类型,包括 byte、short、char 以及 int。但在 JDK 1.5 和 1.7 又分别增加了对枚举类型和 String 的支持。

同时,Switch 语句会跳转到匹配的 case 位置执行剩下的语句,直到最后遇见第一个 break 为止。

Switch 支持 String 的原理

Switch 对 String 类型的支持是利用 String 的 hash 值,本质上还是 switch-int 结构。并且利用了 equals 方法来防止 hash 冲突的问题。最后利用 switch-byte 结构,精确匹配。

public static void main(String[] args) {
    switch (args[0]) {
	    case "A" : break;
        case "B" : break;
        default :
	}
}

// 经过 JVM 编译后:
public static void main(String[] var0) {
    String var1 = var0[0];
    byte var2 = -1;
    switch(var1.hashCode()) {
        case 65:
            if (var1.equals("A")) {
                var2 = 0;
            }
            break;
        case 66:
            if (var1.equals("B")) {
                var2 = 1;
            }
    }
 
    switch(var2) {
        case 0:
        case 1:
        default:
    }
} 

14. 封装

把事物抽象成一个类,将事物拥有的属性和动作隐藏起来,只保留特定的方法与外界联系。当内部的逻辑发生变化时,外部调用不用因此而修改,它们只调用开放的接口,而不用去关心内部的实现。

封装的好处
  1. 只能通过规定的方法访问数据。
  2. 隐藏类的实例细节,方便修改和实现。
  3. 直接通过操控类对象来达到目的,不需要对具体实现十分了解,使类属性和方法的具体实现对外不可见。不但方便还起到了保护作用。
  4. 良好的封装能够减少耦合。
实现步骤
  1. 修改属性的可见性设为private。
  2. 创建getter/setter方法(用于属性的读写)(通过这两种方法对数据进行获取和设定,对象通过调用这两种发方法实现对数据的读写)。
  3. 在getter/setter方法中加入属性控制语句(对属性值的合法性进行判断)。

15. 继承

继承是面向对象的最显著的一个特征。继承是从已有的类(父类或者超类)中派生出新的类(子类),新的类能吸收已有类的数据属性和行为,并能扩展新的能力(方法的覆盖/重写)。JAVA不支持多继承,一个类只能有一个父类。父类是子类的一般化,子类是父类的特殊化(具体化)。

继承的限制

限制1: 一个子类只能够继承一个父类,存在单继承局限。Java之中只允许多层继承,不允许多重继承,Java存在单继承局限。

//多层继承
class A {}
class B extends A {}
class C extends B {}

限制2: 在一个子类继承的时候,实际上会继承父类之中的所有操作(属性、方法),但是需要注意的是,对于所有的非私有(no private)操作属于显式继承(可以直接利用对象操作),而所有的私有操作属于隐式继承(间接完成)。

class A {
	private String msg;
	public void setMsg(String msg) {
		this.msg = msg;
	}
	public String getMsg() {
		return this.msg;
	}
}
class B extends A {
	public void print() {
		//System.out.println(msg); // 错误: msg定义为private,不可见
	}
}
public class TestDemo {
	public static void main(String args[]) {
		B b = new B();
		b.setMsg("张三");
		System.out.println(b.getMsg());
	}
}

对于A类之中的msg这个私有属性发现无法直接进行访问,但是却发现可以通过setter、getter方法间接的进行操作。

限制3: 在继承关系之中,如果要实例化子类对象,会默认先调用父类构造,为父类之中的属性初始化,之后再调用子类构造,为子类之中的属性初始化,即:默认情况下,子类会找到父类之中的无参构造方法。

class A {
	public A() { // 父类无参构造
		System.out.println("*************************") ;
	}
}
class B extends A {
	public B() { // 子类构造
		System.out.println("#########################");
	}
}
public class TestDemo {
	public static void main(String args[]) {
		B b = new B() ; // 实例化子类对象
	}
}

运行结果:

*************************
#########################

这个时候虽然实例化的是子类对象,但是发现它会默认先执行父类构造,调用父类构造的方法体执行,而后再实例化子类对象,调用子类的构造方法。而这个时候,对于子类的构造而言,就相当于隐含了一个super()的形式:

class B extends A {
	public B() { // 子类构造
		super(); // 调用父类构造
		System.out.println("#########################");
	}
}

现在默认调用的是无参构造,而如果这个时候父类没有无参构造,则子类必须通过super()调用指定参数的构造方法:

class A {
	public A(String msg) { // 父类构造
		System.out.println("*************************");
	}
}
class B extends A {
	public B() { // 子类构造
		super("Hello"); // 调用父类构造
		System.out.println("#########################");
	}
}
public class TestDemo {
	public static void main(String args[]) {
		B b = new B(); // 实例化子类对象
	}
}

运行结果:

*************************
#########################

在任何的情况下,子类都逃不出父类构造的调用,很明显,super调用父类构造,这个语法和this()很相似:super调用父类构造时,一定要放在构造方法的首行上。

16. 多态

现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是人,即出现两种形态。

在 Java 中,多态是方法或对象具有多种形态,是面向对象的第三大特征。

多态 就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

即:在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法。

多态的前提

两个对象(类)存在继承关系,多态是建立在封装和继承基础之上的。

多态的理解
  1. 多态是同一个行为具有多个不同表现形式或形态的能力。
  2. 多态就是同一个接口,使用不同的实例而执行不同操作。
多态的实现方式
  1. 通过子类对父类方法的覆盖来实现多态。
  2. 通过一个类中方法的重载来实现多态。
  3. 通过将子类的对象作为父类的对象实现多态。
多态的定义与使用格式

本质就是:父类的引用指向子类的对象。
父类类型 变量名=new 子类类型();

多态的规则
  1. 一个对象的编译类型与运行类型可以不一致。
  2. 编译类型在定义对象时,就确定了,不能改变,而运行类型是可以变化的。
  3. 编译类型看定义对象时 “=” 号的左边,运行类型看 “=” 号的右边。

即:当调用对象方法的时候,该方法会和该对象的运行类型绑定;当调用对象属性时,没有动态绑定机制,即哪里声明,哪里使用。

多态的向上转型
  1. 编译类型看左边,运行类型看右边。
  2. 可以调用父类的所有成员(需遵守访问权限)。
  3. 不能调用子类的特有成员。
  4. 运行效果看子类的具体实现。

即:父类类型 变量名=new 子类类型();

多态的向下转向
  1. 只能强制转换父类的引用,不能强制转换父类的对象。
  2. 要求父类的引用必须指向的是当前目标类型的对象。
  3. 当向下转型后,可以调用子类类型中所有的成员。

即:子类类型 引用名 = (子类类型) 父类引用;

public class Person {
	public void mission() {	
		System.out.println("人要好好活着!");
	}
}

class Student extends Person {	
	@Override
	public void mission() {	
		System.out.println("学生要好好学习!");
	}
	public void score() {
		System.out.println("学生得到好成绩!");
	}
}

public class Test0 {
	public static void main(String[] args) {
		//向上转型(自动类型转换)
		Person p1 = new Student();
		
		//调用的是 Student 的 mission
		p1.mission(); 
		
		//向下转型
		Student s1 = (Student)p1;
		
		//调用的是 Student 的 score
		s1.score();
	}
}

运行结果:

学生要好好学习!
学生得到好成绩!
方法之间的调用优先级:一个经典的多态讲解案例:
class A {
    public String show(D d) {
        return ("A and D");
    }
    public String show(A a) {
        return ("A and A");
    }
}

class B extends A {
    public String show(B b) {
        return ("B and B");
    }
    public String show(A a) {
        return ("B and A");
    }
}
class C extends B {}
class D extends B {}

@Test
void test(){
    A a1 = new A();
    A a2 = new B();

    B b = new B();
    C c = new C();
    D d = new D();

    System.out.println("1--" + a1.show(b));
    System.out.println("2--" + a1.show(c));
    System.out.println("3--" + a1.show(d));
    System.out.println("--------------");
    System.out.println("4--" + a2.show(b));
    System.out.println("5--" + a2.show(c));
    System.out.println("6--" + a2.show(d));
    System.out.println("--------------");
    System.out.println("7--" + b.show(b));
    System.out.println("8--" + b.show(c));
    System.out.println("9--" + b.show(d));
}

输出结果:
image.png

当父类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

产生多态时候,各个方法调用的优先级顺序由高到低依次为:
this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)

  1. System.out.println(“1–” + a1.show(b));
    首先a1是一个标准的A对象的引用变量,传入的参数为标准的 new B(),我们在A类的方法中找不到show(B b)的方法,接着去看A的超类是否有show(B b)的方法,发现A没有超类。那么去执行this.show((super)O),即A类中的show(A a)方法,所以输出为A and A
  2. System.out.println(“2–” + a1.show©);
    同理,a1.show© C的父类是B,所以C也是A的子类,那么最后还是调用A类中的show(A a)方法,所以输出为A and A
  3. System.out.println(“3–” + a1.show(d));
    这个没什么好疑惑的,直接就是调用A类中的show(D obj),所以输出为A and D
  4. System.out.println(“4–” + a2.show(b));
    这个的结果输出就有点迷惑人了,小伙伴们会惊奇,为什么不是调用B类中的show(B b)方法输出B and B ?我们来解释下,a2是一个父类A的引用变量指向了一个子类B的对象,也就是说表面类型是A,实际类型是B。当我们调用方法的时候,首先从其表面类型里边寻找方法 show(B b)结果没有找到,那么按照调用优先级,我们最终会调用到this.show((super)O) 也就是说我们调用了A类中的show(A a)方法。按照上边所述的概念:当父类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。在当前的表面类型中找到了该方法show(A a),并且在子类(B)中也覆盖了该方法,那么最终会调用实际类型(B类)中的该方法show(A a),所以输出B and A。
  5. System.out.println(“5–” + a2.show©);
    这个分析和4中的保持一致
  6. System.out.println(“6–” + a2.show(d));
    这个没什么疑问,在A类中找到了该方法show(D d),并且子类没有覆盖该方法,所以直接调用A类中的方法,输出A and D
  7. System.out.println(“7–” + b.show(b));
    b.show(b)应该没什么疑问,会直接调用B类中的方法show(B b) 输出 B and B
  8. System.out.println(“8–” + b.show©);
    根据方法调用优先级,最终会调用到B类中的方法show(B b) 输出 B and B
  9. System.out.println(“9–” + b.show(d));
    根据方法调用优先级,最终会调用到A类中的方法show(D d) 输出 A and D

17. 重写和重载

重写
  1. 发生在父类与子类之间。
  2. 方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同。
  3. 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)。
  4. 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。
重载
  1. 重载Overload是一个类中多态性的一种表现。
  2. 重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)。
  3. 重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准。

18. Java中的向前引用

所谓向前引用,就是在定义类、接口、方法、变量之前使用它们。

@Test
void test(){
    System.out.println(new MClass().method());
}
class MClass {
    int method() {return n;}
    int n = 1;
}

毫无疑问会输出1,n 在method方法后定义,但method方法可以先使用该变量。

示例
@Test
void test(){
    // 如果简单地执行下面的代码,毫无疑问会输出1.
    System.out.println(new MyClass().method());

    // 使用下面的代码输出变量m,却得到0。
    System.out.println(new MyClass().m);
}
class MyClass {
    int method() {return n; }
    int m = method();
    int n = 1;
}

实际上,从java编译器和runtime的工作原理可以得知。在编译java源代码时只是进行了词法、语法和语义检测,如果都通过,会生成.class文件。不过这时MyClass中的变量并没有被初始化,编译器只是将相应的初始化表达式(method()、1)记录在.class文件中。

当runtime运行MyClass.class时,首先会进行装载成员字段,而且这种装载是按顺序执行的。并不会因为java支持向前引用,就首先初始化所有可以初始化的值。首先,runtime会先初始化m字段,这时当然会调用method方法,在method方法中利用向前引用技术使用了n。不过这时的n还没有进行初始化呢。runtime为了实现向前引用,在进行初始化所有字段之前,还需要将所有的字段添加到符号表中。以便在任何地方(但需要满足java的调用规则)都可以引用这些字段,不过由于还没有初始化这些字段,所以这时符号表中所有的字段都使用默认的值。int类型的字段默认值自然是0了。所以在初始化int m = method()时,method方法访问的n实际上是在进行正式初始化之前已经被添加到符号表中的字段n,而不是后面的int n = 1执行的结果。但将MyClass改成如下的形式,结果就完全不同了。

@Test
void test(){
    // 如果简单地执行下面的代码,输出1.
    System.out.println(new MyClass().method());

    // 使用下面的代码输出变量m,输出1.
    System.out.println(new MyClass().m);
}
class MyClass {
    int method() {return n; }
    int n = 1;
    int m = method();
}

原因是引用初始化m时调用method方法,该方法中使用的n已经是初始化完的了,而不是最初放到符号表中的值。

综合上述,runtime在运行.class文件时,每个作用域(方法、接口、类等带语言元素都有自己的作用域)的符号表都会被至少访问两次:

  1. 第一次会将所有的字段(这里只考虑类的初始化)放到符号表中,暂时不考虑初始化,放到符号表中只是相当于一个索引,好让其他地方引用该字段时可以找到它们。
    例如,method方法中引用n时就会到符号表中寻找n,不过这时的n只是int类型的默认值。
  2. 第二次访问n就是真正初始化n的时候(int n = 1)。这是将符号表中存储的字段n的值更新为实际的初始化值(1)。所以如果引用n放生在正式初始化n之前,当然输出的是0。

19. == 和 equals

==

比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。

  1. 比较的是操作符两端的操作数是否是同一个对象。
  2. 两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。
  3. 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如:int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。
equals

比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以 适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的 equals 方法返回的却是==的判断。

如下为String类的equals方法:

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

注意:
所有比较是否相等时,都是用equals 并且在对常量相比较时,把常量写在前面,因为使用object的
equals object可能为null 则空指针

20. Object 中的公共方法

  1. clone() 创建并返回此对象的副本。
  2. equals() 判断。
  3. getclass() 返回该对象的运行类。
  4. hashcode() 返回对象的哈希码值。
  5. notify() 唤醒正在等待对象监听器的线程。
  6. notifyAll() 唤醒正在等待对象监听器的所有线程。
  7. wait() 导致当前线程等待,直到另一个线程调用该对象的 notify() 或 notifyAll() 方法。
  8. toString() 返回此对象的字符串表示形式。
  9. finalize() 当垃圾收集确定不需要该对象时,垃圾回收器调用该方法。

21. equals 和 hashcode 的联系

hashCode() 是 Object 类的公共方法,返回一个哈希值。如果两个对象根据 equals() 方法比较相等,那么调用这两个对象中任意一个对象的 hashCode() 方法必须产生相同的哈希值,如果两个对象根据 eqauls() 方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的)。

hashCode方法返回对象的哈希值,用于在哈希表中存储对象。如果重写了hashCode方法,那么必须重写equals方法。

equals方法比较两个对象是否相等。如果重写了equals方法,那么必须重写hashCode方法。

为了保证程序的正确性,重写这两个方法时需要遵循一些规则,如:

  1. hashCode方法返回的哈希值必须是相等的对象返回相同的哈希值。
  2. equals方法比较两个对象时,如果返回true,那么两个对象的hashcode也应该相等。
  3. equals方法如果返回true,那么对参数对象和当前对象进行比较应该也应该返回true。
  4. equals方法如果返回false,那么对参数对象和当前对象进行比较应该也应该返回false。
  5. 对于任意非空引用 x , x.equals(null) 返回false。

关于 hashcode 的一些结论:

  1. 两个对象相等,hashcode 一定相等,但两个对象不等,hashcode不一定不等;
  2. hashcode 相等,两个对象不一定相等,但 hashcode 不等,两个对象一定不等。

日常开发法中我们使用java自带的模板,进行重写,或者是使用lombok的注解@EqualsAndHashCode
image.png

22. 深拷贝和浅拷贝

浅拷贝

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
image.png

深拷贝

被复制对象的所有变量都含有与原来的对象相同的值。而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
image.png

23. String str = "a" + "b" + "c";创建了几个对象

在JVM(JDK8)的内存结构中有一块区域叫作字符串常量池,这块区域存储了两样东西,分别是“字面量”和“符号引用”。字面量也就是一串字,例如String str = “abc” 这里的"abc"就是字面量。符号引用是用于定位引用指向的问题。(JDK8完完全全把字符串常量池从方法区搬到堆中了)

一、常量相加
String str = "a" + "b" + "c";

这种情况是常量之间相加,经过编译器优化成了String str = "abc";,所以答案是创建了1个对象。“a”、“b”、"c"这些都是常量,因为它们是final修饰放在字符串常量池中的对象。(注意:字面量也是对象)
image.png

二、变量相加
String a = "a";
String b = "b";
String c = "c";
String str = a + b + c;

这种情况是变量之间相加,已经不是之前的常量相加了,经过编译器优化成了StringBuilder,所以答案是创建了3个对象。new StringBuilder()、new String()、“abc”,因为最终会通过StringBuilder()里面的toString()方法进行new String(“abc”)类型转换。
image.png

三、无优化
String str = "a" + "b" + "c";

不考虑优化的情况下,这一共是创建了5个对象的,因为一个双引号就是一个字面量(对象),这里创建了5个对象,分别是"a"、“b”、“c”、“ab”、“abc”。
image.png

24. String、String StringBuffer 和 StringBuilder

String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符 数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象。

image.png

每次"+“操作:隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法 拼接”+"后面的字符。

同时 JVM 也对 String 拼接做了一定优化,如果几个在编译期就能够确定的字符串常量进行拼接,则直接优化成拼接结果。

String 和 StringBuffer 主要区别是性能上。因为 String 自身是不可变对象,每次对 String 类型进行操作都等同于产生了一个新的 String 对象,然后指向新的 String 对象。所以尽量不要对 String 进行大量的拼接操作,否则会产生很多临时对象,导致 GC 影响系统性能。

因为 StringBuffer 中的每个方法都被 synchronized 修饰,是线程安全的,但也影响了一定性能,故 JDK 1.5 中,新增了 StringBuilder 这个非线程安全类。

StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder 抽象类中我们可以看到他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。
image.png

三者的继承结构

image.png

25. 创建对象的方式

java中提供了以下四种创建对象的方式:new创建反射机制clone机制序列化机制

26. final

用于声明属性、方法和类:

  1. 被final修饰的类不可以被继承。
  2. 被final修饰的方法不可以被重写。
  3. 被final修饰的变量不可以被改变;如果修饰引用,那么表示引用不可变,引用指向的内容可变。
修饰成员变量

如果final修饰的是类变量(static),只能在静态初始化块中指定初始值或声明该类变量时指定初始值。

如果final修饰的是成员变量,可以在非静态初始化块中声明该变量或者构造器中执行初始化。

修饰局部变量

系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值。(仅一次)

修饰基本类型数据和引用类型数据

基本数据类型的变量,一旦初始化之后便不能更改。
如果是引用类型的变量,则在对其初始化之后便不能再让其指向其他对象。但引用的值是可变的。

首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。

注意事项

这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期。

将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?

就将局部娈量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。

被final修饰的方法,JVM会尝试将其内联,以提高运行效率。

被final修饰的常量,在编译阶段会存入常量池中。

注意第三点,无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类或传址的对象来说,被 final 修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。

除此之外,编译器对final域要遵守的两个重排序规则更好: 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之 间不能重排序。初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

27. static

static修饰一个属性字段,那么这个属性字段将成为类本身的资源,public修饰为共有的,可以在类的外部通过test.a来访问此属性;在类内部任何地方可以使用。如果被修饰为private私有,那么只能在类内部使用。

如果属性被修饰为static静态类资源,那么这个字段永远只有一个,也就是说不管你new test()多少个类的对象,操作的永远都只是属于类的那一块内存资源。

字面上,意思是静态的,一旦被static修饰,说明被修饰的对象在一定范围内是共享的,这时候需要注意并发读写的问题。

修饰类成员

static 修饰类成员时,如何保证线程安全是我们常常需要考虑的。当多个线程同时对共享变量进行读写时,很有可能会出现并发问题,如我们定义了:public static List list = new ArrayList();这样的共享变量。这个 list 如果同时被多个线程访问的话,就有线程安全的问题,这时候一般有两个解决办法:

  1. 把线程不安全的 ArrayList 换成 线程安全的 CopyOnWriteArrayList;
  2. 每次访问时,手动加锁。
修饰方法

当 static 修饰方法时,代表该方法和当前类是无关的,任意类都可以直接访问(如果权限是 public 的话)。

有一点需要注意的是,该方法内部只能调用同样被 static 修饰的方法,不能调用普通方法。

我们常用的 util 类里面的各种方法,我们比较喜欢用 static 修饰方法,好处就是调用特别方便,无需每次new出一个对象。

static 方法内部的变量在执行时是没有线程安全问题的。方法执行时,数据运行在栈里面,栈的数据每个线程都是隔离开的,所以不会有线程安全的问题。

修饰代码块

当 static 修饰方法块时,我们叫做 静态代码块 ,静态代码块常常用于在类启动之前,初始化一些值。

public calss PreCache{
    static{
        //执行相关操作
    }
}

28. a=a+b与a+=b

+= 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。如:

byte a = 127;
byte b = 127;
b = a + b; // 报编译错误:cannot convert from int to byte
b += a;

以下代码是否有错,有的话怎么改?

short s1= 1;
s1 = s1 + 1;

有错误。short类型在进行运算时会自动提升为int类型,也就是说 s1+1 的运算结果是int类型,而s1是short 类型,此时编译器会报错。正确写法:

short s1= 1;
s1 += 1;

+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错。

29. continue、break 和 return

  1. continue :指跳出当前的这一次循环,继续下一次循环。
  2. break :指跳出整个循环体,继续执行循环下面的语句。
  3. return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:
    return;:直接使用 return 结束方法执行,用于没有返回值函数的方法。
    return value;:return 一个特定值,用于有返回值函数的方法。
30. Java 异常类层次结构图

image.png

31. Exception 和 Error的区别

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  1. Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  2. Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

32. Checked Exception 和 Unchecked Exception

Checked Exception

即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。

image.png

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException

Unchecked Exception

即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

  1. NullPointerException(空指针错误)
  2. IllegalArgumentException(参数错误比如方法入参类型错误)
  3. NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
  4. ArrayIndexOutOfBoundsException(数组越界错误)
  5. ClassCastException(类型转换错误)
  6. ArithmeticException(算术错误)
  7. SecurityException (安全错误比如权限不够)
  8. UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)

33. Throwable 类常用方法

  1. String getMessage(): 返回异常发生时的简要描述。
  2. String toString(): 返回异常发生时的详细信息。
  3. String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
  4. void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息。

34. try-catch-finally

  1. try块 : 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  2. catch块 : 用于处理 try 捕获到的异常。
  3. finally 块 : 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值