Java SE 不得不注意的地方

0. 前言

       以下内容,是我认为SE部分比较有意思的地方 ,故特此编写。如果有其他重要的点被遗漏, 欢迎评论区补充,后期补充。

1. 面向过程和面向对象

       面向对象和面向过程是一种思想,其并不是由编程语言所实现的。比如我们通常说,C语言是面向过程的,Java是面向对象的。难道C语言就一定不能实现面向对象的思想吗,答案是否定的。

       这里以我个人的观点阐述一下面向过程和面向对象的区别:

       面向过程,通常是站在程序员即我们的角度去看待问题,是第一人称的视角,去一步步想我们应该怎么做才能解决问题。

       面向对象,程序员则是第三人称,通过对需求进行抽象,抽象出一个个的对象,程序员负责将这些对象的相互协作来解决问题。但是每个对象去完成其工作依然属于面向过程。可以这么理解,面向对象在宏观上是多个对象相互合作,微观上依然是面向过程的。

       了解了两者的思想,我们分析一下两者各自的优缺点。

       面向过程的执行优点是速度更加快速,不需要先去创建对象,然后在执行方法。但是带来的问题便是,代码耦合性太高,不够清晰明了,当有新的需求时,需要修改原代码,很难符合开闭原则。

       面向对象的优点则是代码耦合性较低,通过对象间的相互协作,直观明了,并且通过合适的设计模式,可以很容易达到开闭原则。问题则是,创建对象需要时间,执行速度没有面向过程快,并且对象需要占用一定的内存。

2. JDK、JRE 和 JVM 的区别

        JDKJava程序的开发包,而JRE只是Java程序的运行环境,JDK中包含了JRE。对于程序员来说,我们需要JDK开发相关的程序,对于程序的使用者来说,仅需要JRE即可。

在这里插入图片描述

       并且整个Java程序都是运行在JVM之上,我们知道Java语言是跨平台的,其跨平台的原因正是由于JVM的存在,我们的Java程序在Javac编译后,会生成.class文件,该程序一次编译,到处运行。.class文件并不面向任何特定的处理器、操作系统,而只是JVM。如此,即使我们在windows下编译好的.class文件,也可以运行在linux环境下。

在这里插入图片描述

3. 基本数据类型和包装类型

基本数据类型包装类型字节
booleanBoolean-
byteByte1
shortShort2
charCharacter2
intInteger4
floatFloat4
doubleDouble8
longLong8

       一个boolean变量在编译后以一个int代替,一个boolean数组编译后,数组中每个成员是一个byte

       Java语言号称一切皆对象,但是基本数据类型并不属于对象,由此便产生了包装类型。包装类型的作用除了为了表示对象,还可以实现与基本数据类型的自动转换。

       既然包装类型属于对象,那么其与基本数据类型也存在很大的区别:

  1. 初值不同,基本数据类型有其各自的数据类型,而包装类型的初值为null(这里的初值是成员变量而不是方法中的变量,方法中的变量必须赋初值才能使用);
  2. 存储位置不同,基本类型存储位置在栈中,而包装类型的存储位置位于堆中(随着栈逃逸技术的出现,对象不在局限于出现在堆中);
  3. 泛型不能被指定为基本数据类型,只能为引用类型。

4. 自动拆装箱

       自动装箱指的是,基本类型可以自动转换为包装类型,不需要通过new关键字。

       自动拆箱指的是,包装类型可以自动转换为基本数据类型,参与算术运算。

4.1 实现原理

       自动装箱的实现原理:

	public static void main(String[] args) {
        Integer a = 1;
    }

       上述代码中,我们将一个基本数据类型直接赋值给了一个Integer的类型,如此便实现了自动装箱。其编译后的字节码文件如下所示:

在这里插入图片描述

       可以看到,编译器会自动调用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);
    }

       在该方法中,实现了new的过程。

       自动拆箱的实现原理:

    public static void main(String[] args) {
        Integer a = 1;

        int b = a;
    }

       与自动拆箱一样,编译器同样会自动调用Integer.intValue()方法,实现包装类型到基本类型的自动转换。
在这里插入图片描述

	private final int value;

    public int intValue() {
        return value;
    }

4.2 缓存池

       我们知道对于基本类型==的作用是比较两个变量的值,对于引用类型则是比较对象的地址,如果要比较对象的值则可以通过重写equal()方法。

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 1;
        Integer c = new Integer(1);

        System.out.println("a == b, " + (a == b ));
        System.out.println("a == c, " + (a == c ));
    }

       该程序的运行结果如下:

在这里插入图片描述

       首先可以明确的是,上述两个==的作用比较的都是地址。但是为什么会出现不同的结果,答案正是由于缓存池的作用。

       在包装类型中,缓存池的作用是在进行自动装箱时,直接从缓存池中获取对应的对象,否则则是在堆中创建对象。

       我们回到自动装箱的代码中,如下:

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

       首先判断i的值是否在指定的缓存池的范围内,如果在则返回缓存池中的引用,否则去创建对象。那么缓存池是什么?缓存池的大小是多大?

	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() {}
    }

       可以看到缓存池就是该包装类型的一个数组,其可表示的数据范围在-128,127之间。

       在介绍完缓存池后,那么是否是所有的包装类型都存在缓冲池,答案是否定的。

包装类型是否存在缓存池范围
Boolean不存在,但是存在两个常量
Byte存在[-128, 127]
Short存在[-128, 127]
Charcater存在[0, 127]
Integer存在[-128,127]
Float不存在
Double不存在
Long存在[-128,127]

       不知道你有没有写过这样的代码?将一个int直接自动装箱为Double。如果没有的话,可以去尝试一下,结果可能会出乎你的意料。

    public static void main(String[] args) {

        Double a = 1;

        Double b = new Double(1);

    }

       如上所示,你可能会认为Double自动装箱的话调vlaueOf(double d)那么的话,1可以自动类型转换为1.0,那我当然可以实现自动装箱了,然而编译结果如下:
在这里插入图片描述
            可以这样理解,自动装箱的过程中关闭了自动类型转换。

5. Object

       面向对象的三大特征是:封装、继承、多态。

       在Java中,所有的类都是继承自Object类,同时该类的方法在并发中也被经常使用到。

权限方法名作用
publicgetClass()获得Class对象
publichashCode()获得哈希值
publicequals(Object obj)比较两个对象是否相等
publictoString()打印该对象的信息
publicnotify()唤醒一个等待中的线程
publicnotifyAll()唤醒多个等待中的线程
publicwait()让当前线程等待,并释放锁
publicwait(long timeout)让当前线程等待,直到指定时间或被唤醒
publicwait(long timeout, int nanos)同wait(long timeout),time = 1000000*timeout+nanos
protectedclone()克隆对象
protectedfinalize()被垃圾回收器清除之前执行
privateregisterNatives()注册本地方法

       通常,我们比较两个对象是否相等时,不仅会重写equal方法,也会重写hashCode()方法。这样做的意义是,hashCode相同,两个对象的值不一定完全相同。但是hashCode不同,两个对象的值一定不相同。

       当我们往SetMap中添加新元素时,需要先判断插入元素是否存在,首先根据hashCode()方法得到该对象的hashCode值,如果集合里边不存在该值,可以直接插入进去。如果已经存在,则需要再次通过equals()来比较,这样的话可以提升效率。

6. 字符串

6.1 String

       String是经常被使用到的一个数据类型,需要注意的是,其是引用类型,并不是基本类型。

       我们都知道String是一个不可变字符串,那么为什么是不可变字符串?是因为String数组内部封装了一个final数组。

	private final char value[];

       我们知道对于被final修饰的引用类型,只是其地址值不可以改变,但是其元素还是可以被改变的,那么还可以称为不可变字符串吗?

       倘若str = "abc",那么则说明value = [‘a’, ‘b’, ‘c’],如果我们令str="abcd",此时是没办法实现的,因为value不能指向一个新的数组,所以从这个角度来看,可以理解为不可变字符串,并且value是由private修饰外界且没有向外界提供访问的接口,更是无法直接修改value数组。

       存在如下一段代码:

	String s1 = "abc";
    String s2 = "abc";
    String s3 = new String("abc");
    System.out.println(s1 == s2);
    System.out.println(s1 == s3);

       输出如下:

	true
	false

       这时因为每次new都是在堆中分配一块内存,然后将该内存的引用返回。

       如果字符串的常量池中已经存在对应的常量,直接返回对应的地址即可。

       String s3 = new String(“abc”),因为字符串常量池已经存在对用的字符串常量,所以内存图如下:

在这里插入图片描述

       在String中,有一个方法即为有趣,即intern(),该方法的作用是如果常量池中存在和目标串相同的值,则返回常量池中的地址。否则,直接返回引用,并且将目标串加入到字符串常量池中

       示例如下:

	String s1 = new String("abc");
    String s2 = s1.intern();
    System.out.println(s1 == s2);
    // 输出:false

在这里插入图片描述

	String s1 = "abc";
    String s2 = s1.intern();
    System.out.println(s1 == s2);
    // 输出:true

6.2 StringBuilder

       在说到String这个不可变字符串时,不得不说到可变字符串StringBuildStringBuffer。因为我们有的时候并不想为了修改字符串而频繁的去创建String对象,由此便有了可变字符串。

       StringBuild的源码如下所示:

public final class StringBuilderextends AbstractStringBuilderimplements java.io.Serializable, CharSequence{
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

       可以看到其value属性并不是由final所修饰。

       在我们创建StringBuilder对象时,默认创建一个长度为16的字符数组。

 	public StringBuilder() {
        super(16);
    }

6.3 StringBuffer

       StringBufferStringBuilder的区别是StringBuffer是线程安全的,其方法基本都是由synchronized所修饰。

在这里插入图片描述

6.4 其他问题

       不可变字符串和可变字符串各自的优缺点是什么?为什么要将String设计成不可变字符串?

       该部分内容转载自:String 为什么要设计成不可变?

       不可变字符串的优点有:

  1. 便于实现字符串常量池

       在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。

String a = "Hello world!";
String b = "Hello world!";

       如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!

  1. 使多线程安全

       有如下这个场景,一个函数appendStr()在不可变的String参数后面加上一段“bbb”后返回。appendSb()负责在可变的StringBuilder后面加"bbb"

public class test {
  // 不可变的String
  public static String appendStr(String s) {
      s += "bbb";
      return s;
  }

  // 可变的StringBuilder
  public static StringBuilder appendSb(StringBuilder sb) {
      return sb.append("bbb");
  }
  
  public static void main(String[] args) {
      String s = new String("aaa");
      String ns = test.appendStr(s);
      System.out.println("String aaa>>>" + s.toString());
      // StringBuilder做参数
      StringBuilder sb = new StringBuilder("aaa");
      StringBuilder nsb = test.appendSb(sb);
      System.out.println("StringBuilder aaa >>>" + sb.toString());
  }
}

       如果程序员不小心像上面例子里,直接在传进来的参数上加上“bbb”。因为Java对象参数传的是引用,所有可变的StringBuffer参数就被改变了。可以看到变量sbTest.appendSb(sb)操作之后,就变成了"aaabbb"
       有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。

       在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。

  1. 避免安全问题

       在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。

       因为String是不可变的,所以它的值是不可改变的。但由于String不可变,也就没有任何方式能修改字符串的值,每一次修改都将产生新的字符串,如果使用char[]来保存密码,仍然能够将其中所有的元素设置为空和清零,也不会被放入字符串缓存池中,用字符串数组来保存密码会更好。

  1. 加快字符串处理速度

       由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String

       在String类的定义中有如下代码:

private int hash;//用来缓存HashCode

       总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。

       不可变字符串的缺点有:

       在优点中,我们已经说明了其缺点,便是每对String对象做一次修改,都会重新创建一个对象,并放入到String Pool中(如果String Pool中不存在该对象),如果频繁的修改对象便造成大量空间的浪费。

       而可变字符串的优点则正是用来弥补不可变字符串的缺点,不可变字符串的优点同样是用来弥补可变字符串的缺点。凡事都有两面性,所以我们需要在合适的场景选择合适的字符串。

7. 抽象类与接口

       抽象类的作用往往是定义一个模板,然后其子类照着这个模板去实现相关模块,如前面说到的StringBuilder,其便是继承自一个AbstractStringBuilder

       接口的作用往往是定义有哪些行为,接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。

       抽象类与普通类的区别:

  1. 类名由abstract所修饰
public abstract class Person {
}
  1. 类名不能被final修饰

  2. 可以不包含方法的实现

public abstract class Person {

	//如果方法被abstract修饰,说明这是一个抽象方法
    public abstract void sayHello();
}
  1. 不能实例化

       这里有一点值得注意,便是抽象类不能被实例化,那么其构造方法存在的意义是什么?

public abstract class Person {
    public abstract void sayHello();

    public Person(){
        System.out.println("执行抽象类的构造方法");
    }
}

class Student extends Person{

    @Override
    public void sayHello() {
        System.out.println(getClass().getName()+":sayHello");
    }

    public Student(){
        System.out.println("执行非抽象类的构造方法");
    }

    public static void main(String[] args) {
        Student student = new Student();
        student.sayHello();
    }
}

       上述代码的输入结果如下:

	执行抽象类的构造方法
	执行非抽象类的构造方法
	Student:sayHello

       可以看出,在执行子类的构造器时,会执行抽象类的构造方法,所以抽象类的构造方法可以完成一些初始化操作。

       接口和抽象类的区别:

  1. 接口可视作比抽象类更加抽象的类。
  2. 接口中的方法只能为public,对于抽象方法而言,其只是不能被private修饰。
  3. 接口没有构造方法。
  4. 接口中的变量默认被public static final所修饰。
  5. JDK1.8及以后,接口允许有方法体,因为当我们给接口拓展方法时,其每一个实现类都要重写该方法,这样会很不便。
public interface MyInterface {

    void a();

	//如果方法具有方法体,且非静态方法,必须被default修饰
    default void b(){
        System.out.println("hello interface");
    }

}

8. 重载和重写

       重载发生在同一个类中,表示方法名相同的情况下,允许参数列表的个数和类型不同。

interface Language{

}

class Java implements Language {

}

class Python implements Language {

}

class Go implements Language {

}

class Stu{

    public void Study(Java java){
        System.out.println("Study Java");
    }

    public void Study(Python python){
        System.out.println("Study Python");
    }
    
    private void Study(Go go){
        System.out.println("Study Go");
    }
}

       重写则是发生在父子类之间,子类可以重写父类的方法。

class Father{
    public void print(){
        System.out.println("Father");
    }
}

class Son extends Father{
    public void print(){
        System.out.println("Son");
    }
}

       重载适用于同一个类的多个方法,这些方法功能是一致的,但细节可能不一致,如参数类型、个数彼此不同

       重写适用于父类方法不能满足子类的要求,但是子类要实现的功能和父类一致

9. 多态

       多态指的是一个对象可以有多种类型,分为编译时类型和运行时类型。编译时类型是在编译阶段就可以确定的,运行时则只能在程序运行的过程中确定。

public class Test {
    public static void main(String[] args) {
        Language language = null;
        language = new Java();
        language = new Python();
        language = new Go();
    }
}

interface Language{

}

class Java implements Language {

}

class Python implements Language {

}

class Go implements Language {

}

       上述代码中,language编译时类型为Language,运行时类型则为JavaPython以及Go

       多态下,成员访问的规则如下:

       成员变量:编译看左边,运行看左边
       成员方法:编译看左边,运行看右边

10. 泛型

       可参考如下博客:

       泛型就这么简单

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值