Java基础知识总结(上)

简介: Java基础知识总结(上)

Java中的数据类型


Java的数据类型分为两大类:基本类型和引用类型


  • 引用类型:引用类型指向一个对象,不是原始值,指向对象的变量称为引用变量。Java中除了基本类型,其他都是引用类型,如类(String类)、接口、数组、枚举、注解等。将引用存放栈中,实际存放值在堆内存。
  • 基本数据类型(也称值类型):整型(byte/short/int/long)、浮点型(float/double)、字符型(char占两个字节)与布尔型(boolean占一个字节)。直接在栈中存储数值。编程语言内置的最小粒度的数据类型,共有四大类八种基本数据类型。具体如下表:


image.png

标识符和关键字的区别是什么?Java关键字?


在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。


Java中的关键字:


image.png

自动拆装箱、发生与实现?


每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可,比较两个包装类数值要用 equals ,而不能用 == 。


  • 自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。
  • 使用基础型类给引用类型变量赋值;
  • 具体实现:调用引用类型对应的静态方法valueOf,本质是在该方法内部调用构造函数创建对象。


自动装箱的缓存机制:为了节省内存和提升性能,Java给多个包装类提供了缓存机制,可以在自动装箱过程中,把一部分对象放到缓存中(引用常量池),实现了对象的复用。如Byte、Short、Integer、Long、Character等都支持缓存。


  • 自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
  • 当基础类型与引用类型进行 “==、+、-、×、÷” 运算时,会对引用类型进行自动拆箱;
  • 具体实现:引用类型对象内部包含对应基本类型的成员变量,自动拆箱时返回该成员变量即可;

switch支持哪些类型?

  • jdk1.6之前只支持int、char、byte、short这样的整型的基本类型或对应的包装类型Integer、Character、Byte、Short常量,包装类型最终也会经过拆箱为基本类型,本质上还是只支持基本类型;JDK1.5开始支持enum,原理是给枚举值进行了内部的编号,进行编号和枚举值的映射
  • JDK1.7开始支持String,但不允许为null,原理是借助 hashcode( ) 来实现。


注意:null的hashCode()没有,hashmap是人为写到0槽的!


== 与 equals 区别


"=="对比的是栈中的内容:


  • 基本数据类型比较的是变量值;
  • 引用类型比较的堆中内存对象的地址。


equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值(内容)比较。


注:对于"=="中引用类型,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。因为每new一次,都会重新开辟堆内存空间。


String中重写equals方法举例:

public boolean equals(Object anObject) {
    // 1. 如果比较的两个对象的首地址是相同的,那指的肯定是同一个对象
    if (this == anObject) {
        return true;
    }
    // 2. 两个对象的首地址不相同,比较内容是否相同
    // instanceof:判断传入的实参是否为String类型,因为形参类型是固定的(重写的要求),所以需要判断
    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) {
                //实质比较字符的ascii值,即两个字符串内容的比较!
                if (v1[i] != v2[i])   
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}


重写思路:


  • 判断两个对象的内存首地址
  • 判断传入形参的类型是否为String类型
  • 判断传入对象的长度
  • 判断字符的ascii码,即比较内容


应用案例分析:

String str1 = "Hello";      // 分配到常量池
String str2 = new String("Hello");   // 在堆中分配内存
String str3 = str2;   // 引用传递
System.out.println(str1 == str2); // false,比较两者栈中的内存地址,显然不同
System.out.println(str1 == str3); // false
System.out.println(str2 == str3); // true
System.out.println(str1.equals(str2)); // true,string重写了equals方法,实际上对比的两个字符串的内容
System.out.println(str1.equals(str3)); // true
System.out.println(str2.equals(str3)); // true


注意:字符串/包装类对象值的比较必须使用equals(),即对比内容。比如下边的Integer缓存-128~127的整型值,如果超过这个范围,就会在堆上创建对象,不会复用已有的对象。


int和Integer的区别(包装类与基本类型的区别)


  • Integer是int的包装类,int则是java的一种基本数据类型
  • Integer变量必须实例化后才能使用,而int变量不需要
  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。
  • Integer的默认值是null,int的默认值是0


Integer a= 127与 Integer b = 127相等吗?(常量池缓存技术)


写在前:Java 基本类型的包装类的大部分都实现了常量池技术。


Byte,Short,Integer,Long 这 4 种整型的包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。


注意:一个字节有8位。最大正数二进制是0111 1111 = 64+32+16+8+4+2+1=127;最小负数二进制是1000 0000→ 反码:1111 1111→ 补码: -{(1+2+4+8+16+32+64)+1} =-(127+1)=-128,整形的缓存范围都是[-128,127]。


对于对象引用类型:==比较的是对象的内存地址,对于基本数据类型:==比较的是值。

Integer内部有一个IntegerCache的内部类。对整型值在-128到127之间的对象进行缓存。缓存会在Integer类第一次使用的时候被初始化出来,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象(直接从缓存中取出),超过范围 a1==b1的结果是false。


注:若数值超过缓存范围,则需要在堆中创建新的对象!

public static void main(String[] args) {
    Integer a = new Integer(1);
    Integer b = 1;  // 将1自动装箱成Integer类型,实际是调用静态方法valueOf()
    int c = 1;
    System.out.println(a == b);  // false,引用了不同对象
    System.out.println(a == c);  // true,a自动拆箱与c比较
    System.out.println(b == c);  // true
    Integer a1 = 128;
    Integer b1 = 128;
    System.out.println(a1 == b1);  // false, 自动装箱,new新的Integer对象
    Integer a2 = 127;
    Integer b2 = 127;
    System.out.println(a2 == b2);  // true,字面值介于-128与127, 自动装箱不会new新的Integer对象,直接引用常量池中的对象
}


java有了基本类型,为什么还要设计对应的引用类型?


  • 可以使用为该引用类型而编写的方法
  • Java集合(map、set、list)等所有集合只能存放引用类型数据,不能存放基本类型数据(容器中实际存放的是对象的引用)。
  • 引用类型对象存储在堆上,可以控制其生命周期;而基本类型存储在栈上,会随着代码块运行结束被回收
  • Java泛型使用类型擦除法实现,基本类型无法实现泛型。


为什么会设置包装类型?


  • Java是一门面向对象的编程语言,但是Java中的基本数据类型却不是面向对象的,并不具有对象的性质,这在实际生活中存在很多的不便。为了让基本类型也具有对象的特征,就出现了包装类型,使得Java具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作,方便涉及到对象的操作。
  • 比如,我们在使用集合类型时,就一定要使用包装类型,因为容器都是装object的,基本数据类型显然不适用。
  • 逻辑上来讲,java只有包装类就够了,为了运行速度,需要用到基本数据类型。实际上,任何一门语言设计之初,都会优先考虑运行效率的问题,所以二者同时存在是合乎情理的。


java泛型相关问题


在 jdk 1.5 之前没有泛型的情况的下只能通过对类型 Object 的引用来实现参数的任意化,其带来的缺点是要做显式强制类型转换,而这种强制转换编译期是不做检查的,容易把问题留到运行时,会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

import java.util.ArrayList;
import java.util.List;
public class Test001 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("java");
        list.add("非泛型");
        list.add(100);
        // 泛型使用(jdk1.7实例化类型可以自动推断)
        List<String> list1 = new ArrayList<>();
        list1.add("java");
        list1.add("泛型");
        // 想加入一个Integer类型的对象时会出现编译错误
        //list1.add(100);
        for (int i = 0; i < list1.size(); i++) {
            String name1 = list1.get(i);
            System.out.println("name1:" + name1);
        }
        System.out.println();
        for (int i = 0; i < list.size(); i++) {
            // Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
//            String name = (String) list.get(i);
//            System.out.println("name:" + name);
        }
        // 泛型擦除(泛型只存在编译阶段,成功编译过后的class文件中是不包含任何泛型信息的)
        List<String> name = new ArrayList<String>();
        name.add("java");
        List<Integer> age = new ArrayList<Integer>(712);
        System.out.println("name class:" + name.getClass());
        System.out.println("age class:" + age.getClass());
        System.out.println(name.getClass() == age.getClass());
        List<Object> objectList = new ArrayList<>();
        List<String> stringList = new ArrayList<>();
        // compilation error incompatible types
        // objectList = stringList;
    }
}


在如上的编码过程中,我们发现主要存在两个问题:


  • 当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,该对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。
  • 因此,取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。


泛型是jdk1.5的新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。


好处


  • 类型安全,编译期检查,不存在 ClassCastException。
  • 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。
  • 代码重用,合并了同类型的处理代码。


泛型擦除是什么?介绍下常用的通配符


原理:泛型只存在于编译阶段,不存在与运行阶段(编译后的class文件不存在泛型的概念)。例如定义 List<Object> 或 List<String>,在编译后都会变成 List 。


Java中List和原始类型List之间的区别?


  • 原始类型和带参数类型< Object >之间的主要区别:在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。
  • 你可以把任何带参数的类型传递原始类型List,但却不能把List< String >传递给接受List< Object >方法,因为会产生编译错误。


定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为 Object,如果有限定类型就会替换为第一个限定类型,例如 <T extends A & B> 会使用 A 类型替换 T。


常用的通配符为: T,E,K,V,?


(1)? 表示不确定的 java 类型

(2)T (type) 表示具体的一个 java 类型

(3)K V (key value) 分别代表 java 键值中的 Key Value

(4)E (element) 代表 Element


什么是泛型中的限定通配符和非限定通配符 ? List<? extends T>和List <? super T>之间有什么区别 ?


  • 限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
  • 另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。


总结:extend用于设定类型的上界(必须是T的子类),super用于设定类型的下界(必须是T的父类)


编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。


public V put(K key, V value) {
    return cache.put(key, value);
}


Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。


浅谈String str = "" 和 new String()的区别


两者看似都是创建了一个字符串对象,但在内存中确是各有各的想法。


  • String str1= “abc”; 在编译期,JVM会去常量池来查找是否存在“abc”,如果不存在,就在常量池中开辟一个空间来存储“abc”;如果存在,就不用新开辟空间。然后在栈内存中开辟一个名字为str1的空间,来存储“abc”在常量池中的地址值。
  • String str2 = new String("abc") ;  在编译阶段JVM先去常量池中查找是否存在“abc”,如果不存在,则在常量池中开辟一个空间存储“abc”。在运行时期,通过String类的构造器(intern()方法)在堆内存中new了一个空间,然后将String池中的“abc”复制一份存放到该堆空间中,在栈中开辟名字为str2的空间,存放堆中new出来的这个String对象的地址值。


也就是说,前者在初始化的时候可能创建了一个对象,也可能一个对象也没有创建;后者因为new关键字,至少在内存中创建了一个对象,也有可能是两个对象。


什么是字符串常量池(String Pool)?


  • 字符串常量池位于堆内存中(java8之前在方法区,java8之后被放到堆内存),专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串;
  • 在创建字符串时(new String()),JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串通过 String 的 intern() 方法放到池中,并返回其引用


ps:池化(如Java线程池、连接池与内存池等)的优点:


  • 降低资源消耗:通过池化技术重复利用已经创建的资源,提高内存的使用率
  • 提高响应速度:如果字符串已经存在直接返回它的引用,如果没有池化的字符串则是通过String的intern()方法放入池中,然后再返回它的引用。
  • 便于资源的管理。


String a = "a" + new String("b")创建了几个对象?


  • 常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。
  • 使用字面量时只创建一个常量池中的常量。
  • 使用 new 时如果常量池中,如果有,直接让引用指向常量池的对象;没有该值就会在常量池中新创建,再在堆中创建一个对象,引用常量池中常量。
  • 因此 String a = "a" + new String("b") 会创建三个或者四个对象,常量池中的 a 和 b,堆中的 b(常量池中存在,则无需创建) 和堆中的 ab。


字符串拼接有哪几种方式?


  • 直接用 + ,底层用 StringBuilder 实现。只适用小数量。如果频繁(或者在循环体中)的使用 + 拼接,相当于不断创建新的 StringBuilder 对象(sppend进行拼接)再转换成 String 对象(toString进行拼接),效率极差,内存消耗大。
  • 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +

public String concat(String str) {
   int otherLen = str.length();
   if (otherLen == 0) {
       return this;
   }
   int len = value.length;
   char buf[] = Arrays.copyOf(value, len + otherLen);
   str.getChars(buf, len);
   return new String(buf, true);
}
  • 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。

public synchronized StringBuffer append(String str) {
   toStringCache = null;
   super.append(str);
   return this;
}


  • StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,并且,Java8中的String类中也提供了一个静态的join方法,用法和StringUtils.join类似。底层也是通过StringBuilder进行拼接。


String 常用的方法?


  • 判断功能
    boolean equals(Object obj):比较字符串的内容是否相同,区分大小写a A
    boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写 a A
    boolean contains(String str):判断大字符串中是否包含小字符串
    boolean startsWith(String str):判断字符串是否以某个指定的字符串开头
    boolean endsWith(String str):判断字符串是否以某个指定的字符串结尾
    boolean isEmpty():判断字符串是否为空。
  • 获取功能
    int length():获取字符串的长度。
    char charAt(int index):获取指定索引位置的字符
    int indexOf(String str):返回指定字符串在此字符串中第一次出现处的索引。
    int indexOf(int ch,int fromIndex):返回指定字符在此字符串中从指定位置后第一次出现处的索引。
    int indexOf(String str,int fromIndex):返回指定字符串在此字符串中从指定位置后第一次出现处的索引。
    String substring(int start):从指定位置开始截取字符串,默认到末尾。
    String substring(int start,int end):从指定位置开始到指定位置结束截取字符串。
  • 转换功能
    byte[] getBytes():把字符串转换为字节数组。
    char[] toCharArray():把字符串转换为字符数组。
    static String valueOf(char[] chs):把字符数组转成字符串。
    static String valueOf(int i):把int类型的数据转成字符串。注意:String类的valueOf方法可以把任意类型的数据转成字符串。
    String toLowerCase():把字符串转成小写。
    String toUpperCase():把字符串转成大写。
    String concat(String str):把字符串拼接。
  • 其他方法
    String rerplace(char old,char new);返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
    String replace(String old,String new);返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
    String trim(); 去掉字符串两端的空格


字符串的" "与null的区别:


  • " "是字符串常量.同时也是一个String类的对象,既然是对象当然可以调用String类中的方法;
  • Null是空常量,不能调用任何的方法,否则会出现空指针异常,null常量可以给任意的引用数据类型赋值


ps:String创建字符串的方式:直接赋值(在常量池中,只开辟了一块内存,并且会自动入池,不会产生垃圾)和实例化方式(通过构造函数创建的字符串对象在堆中,会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过public String intern();方法进行手工入池,通常在开发的过程中不会采用构造方法进行字符串的实例化。)


String是否可变?


String类就是final修饰(字符数组final修饰)!Jdk8中String类有两个成员变量:

/** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0


也就是说在String类内部,一旦初始化就不能被改变。所以可以认为String对象是不可变的。


ps:在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串 private final byte[] value。


String为什么要设计为不可变?(String的优点)


  • 效率(字符串常量池),字符串常量池的需要,只有字符串不可变时,字符串常量池才能实现。
  • 安全(多线程、hash值唯一、类加载,参数安全)。
  • 多线程安全,对象是只读的,不会出现线程安全问题。
  • 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
  • 类加载器要用到字符串,不可变提供了安全性,以便类被正确地加载。
  • String被许多的Java类(库)用来当做参数,例如网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。


String 是不可变类为什么值可以修改?


  • String 类和其存储数据的成员变量 value 字符数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象(只是修改变量引用的对象)。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。
  • 用反射,可以反射出String对象中的value属性, 进而通过获得的value引用改变数组的结构。可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。


Java里实现不可变类的四大要素:


  • 尽量使用final修饰所有的属性(field)
  • 尽量使用private修饰属性。
  • 禁止提供可改变实例状态的公开接口
  • 禁止不可变类被“外部”继承
  • 使用final关键字修饰类
  • 构造器私有化 & 提供静态构造方法


String、StringBuffer与StringBuilder的区别和应用场景


是否产生新对象:


  • String是final修饰的,不可变,每次操作都会产生新的String对象(一定程度上导致了内存浪费)。
  • StringBuffer和StringBuilder都是在原对象上进行操作(不会产生新对象)。


频繁的对String对象进行修改,会造成很大的内存开销:

// str指向了一个String对象(内容为“hello”)
String str = “hello";
// 对str进行“+”操作,str原来指向的对象并没有变,而是str又指向了另外一个对象(“hello world”),原来的对象还在内存中。
str = str + "world“;


注意:String类是final修饰,不能被继承和重写,实现了equals()和hashCode()方法。

三者区别(线程安全性与性能):


  • 线程安全性:StringBuffer是线程安全的(内部方法都用synchronized关键字修饰),StringBuilder是线程不安全的;String不可变性保证线程安全(可理解为常量)。
  • 性能(效率):StringBuilder>StringBuffer>String;ps:相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升。


注:这里谈到一个对象是否线程安全,是不是需要额外进行加锁,保证满足三个条件:多线程环境下、变量为共享变量和结果不受影响。


应用场景:操作少量数据使用String;StringBuffer和StringBuilder经常需要改变字符串内容时使用:单线程操作字符串缓冲区下⼤量数据使用StringBuilder;多线程(共享变量)操作字符串缓冲区下⼤量数据为了保证结果的正确性使用StringBuffer。


构造方法:作用、特性


  • 主要作⽤是完成对类对象的初始化⼯作。注意:在调⽤⼦类构造⽅法之前会先调⽤⽗类没有参数的构造⽅法,目的是帮助子类初始化。


构造方法特性(特别注意):


  • 名字与类名相同,没有返回值,但不能⽤ void 声明构造函数。
  • ⽣成类的对象时⾃动执⾏,⽆需调⽤。
  • ⼀个类即使没有声明构造⽅法也会有默认的不带参数的构造⽅法。
  • 构造方法不能被继承,构造方法只能被显式或者隐式地调用。
  • 子类的构造方法总是先调用父类的构造方法,如果子类的构造方法没有显式地指出使用父类的哪个构造方法,子类则默认调用父类的无参构造方法(此时若父类自定义了构造方法,而子类又没有用super则会报错)。


ps:Java不支持像C++中那样的复制构造方法(没有这个概念),但是在Object类中有一个clone()方法。


关于 static 关键字的⼀些总结


static关键字常见的用法是修饰变量和方法,其次可以修饰代码块,比较少的应用是修饰类(匿名内部类)。


static关键字最基本的用法是修饰变量与方法


  • 被static修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要new出一个类来
  • 被static修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要new出一个类来


被static修饰的变量、被static修饰的方法统一属于类的静态资源,是类实例之间共享的,换言之,一处变、处处变。JDK把不同的静态资源放在了不同的类中而不把所有静态资源放在一个类里面,很多人可能想当然认为当然要这么做,但是是否想过为什么要这么做呢?个人认为主要有三个好处:


  • 不同的类有自己的静态资源,这可以实现静态资源分类。比如和数学相关的静态资源放在java.lang.Math中,和日历相关的静态资源放在java.util.Calendar中,这样就很清晰了
  • 避免重名。不同的类之间有重名的静态变量名、静态方法名也是很正常的,如果所有的都放在一起不可避免的一个问题就是名字重复,这时候怎么办?分类放置就好了。
  • 避免静态资源类无限膨胀,这很好理解。


静态资源属于类,但是是独立于类存在的。从JVM的类加载机制的角度讲,静态资源是类初始化的时候加载的,而非静态资源是类new的时候加载的。类的初始化早于类的new,比如Class.forName(“xxx”)方法,就是初始化了一个类,但是并没有new它,只是加载这个类的静态资源罢了。所以对于静态资源来说,它是不可能知道一个类中有哪些非静态资源的;但是对于非静态资源来说就不一样了,由于它是new出来之后产生的,因此属于类的这些东西它都能认识。故:


  • 静态方法能不能引用非静态资源?不能,new的时候才会产生的东西,对于初始化后就存在的静态资源来说,根本不认识它。
  • 静态方法里面能不能引用静态资源?可以,因为都是类初始化的时候加载的,大家相互都认识。
  • 非静态方法里面能不能引用静态资源?可以,非静态方法就是实例方法,那是new之后才产生的,那么属于类的内容它都认识。


静态代码块也是static的重要应用之一。也是用于初始化一个类的时候做操作用的,和静态变量、静态方法一样,静态块里面的代码只执行一次,且只在初始化类的时候执行。静态块很简单,不过提三个小细节:


  • 静态资源的加载顺序是严格按照静态资源的定义顺序来加载的
  • 静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问(如print()操作)。
  • 静态代码块是严格按照父类静态代码块->子类静态代码块的顺序加载的,且只加载一次


static修饰类:这个用得相对比前面的用法少多了,static一般情况下来说是不可以修饰类的,如果static要修饰一个类,说明这个类是一个静态内部类(注意static只能修饰一个内部类),也就是匿名内部类。像线程池ThreadPoolExecutor中的四种拒绝机制CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy就是静态内部类。


静态方法 与 实例方法


早期的结构化编程,几乎所有的方法都是“静态方法”,引入实例化方法概念是面向对象概念出现以后的事情了,区分静态方法和实例化方法不能单单从性能上去理解(两者在性能上,如加载时机,和内存占用都一样,调用速度也没有太大区别),创建c++,java,c#这样面向对象语言的大师引入实例化方法一定不是要解决什么性能、内存的问题,而是为了让开发更加模式化、面向对象化。这样说的话,静态方法和实例化方式的区分是为了解决模式的问题。


静态方法和实例方法的区别主要体现在两个方面:


外部调用 访问特点
静态方法 类名.方法名/对象名.方法名 只允许访问静态成员(即静态成员变量和静态方法)
实例方法 对象名.方法名 访问无限制


静态方法只能访问静态成员,实例方法可以访问静态和实例成员!


之所以不允许静态方法访问实例成员变量,是因为实例成员变量是属于某个对象的,而静态方法在执行时,并不一定存在对象。同样,因为实例方法可以访问实例成员变量,如果允许静态方法调用实例方法,将间接地允许它使用实例成员变量,所以它也不能调用实例方法。基于同样的道理,静态方法中也不能使用关键字this。


关于 final 关键字的⼀些总结


final 关键字主要应用位置:常量、变量、方法和类(除抽象类)


  • 被final修饰的常量:在编译阶段会存入调用类的常量池中,具体参见类加载机制最后部分和Java内存区域
  • 被final修饰的变量不能被改变: 对于⼀个 final 变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。切记不可变的是变量的引用!,但内容可以进行改变。
  • 被final 修饰的类不能被继承。final 类中的所有成员⽅法都会被隐式地指定为 final ⽅法。
  • 被final修饰的方法不能被重写:目的是把⽅法锁定,以防任何继承类修改它的含义。


final关键字的好处:


  • final方法比非final快一些
  • final关键字提高了性能。JVM和Java应用都会缓存final变量。
  • final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
  • 使用final关键字,JVM会对方法、变量及类进行优化。


ps:final不能修饰抽象类和接口!!!


final finally finalize区别


  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值(引用类型变量初始化后不能在指向另一个对象)。
  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。


try-catch-finally相关


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


异常处理中return的执行顺序:


(1)执行try的语句块

(2)转至catch块,再执行finally块,try块的return永远不会执行.

(3)若finally块中有return,则返回值;

(4)若finally块中没有return,则返回catch块的return值 (此时catch块中的return值是暂存的)


finally语句块什么情况不会执行


  • finally语句块第一行出现异常
  • 程序抛出异常(或者return)之前,调用system.exits(int),退出程序
  • 程序抛出异常(或者return)之前,线程死亡
  • 程序抛出异常(或者return)之前,关闭CPU


注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值!!!

public class Test {
    public static int f(int value) {
        try {
            return value * value;
        } finally {
            if (value == 2) {
                return 0;
            }
        }
    }
}


如果调用 f(2),返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。


Java异常常见问题


异常是什么:java.lang.Throwable派生出ErrorException。Exception又分为运行时异常非运行时异常。


  • error:程序(JVM)无法处理的严重错误 ,不能通过 catch 来进行捕获(智能尽量避免),通常会导致程序终止。例如系统崩溃、内存不足、虚拟机运行错误、类定义错误等;
  • exception:程序可以处理的异常,需要对其进行处理 ;


Exception分类


  • 运行时异常(非受检异常):继承自 RuntimeException,即使不做处理也可以通过编译。RuntimeException及其子类都统称为非受检查异常,例如:NullPointExecrption、NumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。可以由程序员自己决定(因为运行时异常中有很多是代码本身写错了,需要的不是处理异常,而是修改代码,如空指针异常、数据访问越界异常)。
  • 非运行时异常(受检异常):必须对其进行处理,需要用 try...catch... 语句捕获并进行处理(如果不进catch/throw处理的话,就没办法通过编译),并且可以从异常中恢复。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检异常。常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException


java常见的异常


  • java.lang.InstantiationError:实例化错误。当一个应用试图通过new操作符构造一个抽象类或者接口时抛出该异常.
  • java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
  • java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
  • java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
  • java.lang.NullPointerException:空指针异常
  • java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。


Throwable类常用方法


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


Object 类有哪些方法?


Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public final native Class<?> getClass()
//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public native int hashCode() 
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。
public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。
public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作


equals:检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。


hashCode:散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,因此 hashCode 是对象相等的必要不充分条件。


toString:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。

clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方抛出一个** CloneNotSupport异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。


finalize:确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在 JDK 9 被标记为过时方法,并不适合释放资源。


getClass:返回包含对象信息的类对象。


wait / notify / notifyAll:阻塞或唤醒持有该对象锁的线程。


拓展:为什么Java把wait与notify放在Object中?


  • 功能角度
    1)wait与notify的原始目的,是多线程场景下,某条件触发另一逻辑,该条件对应的直接关系为某种对象,进而对应为Object,其对应为内存资源。
    2)Thread对应为CPU,与具体条件不是直接关系,Thread是对象的执行依附者。
  • 内存角度
    1)线程的同步需要Monitor的管理,其与实际操作系统的重型资源(锁)相关。
    2)只有涉及多线程的场景,才需要线程同步,如果wait与notify放在Thread,则每个Thread都需要分配Monitor,浪费资源。
    3)如果放在Object,单线程场景不分配Monitor,只在多线程分配。分配Monitor的方法为检测threadId的不同。


获取键盘输入常用的两种方法


  • Scanner

Scanner input = new Scanner(System. in);
String S = input .nextLine();
input. close();


  • BufferedReader (new InputStreamReader(System. in))

BufferedReader input = new BufferedReader(new InputStreamReader(System. in));
String S = input.readLine();
BufferedReader input = new BufferedReader(new FileReader(file));  // 按行读取文件!


Java引用和C指针的区别


  • 现象:指针在运行时可以改变其所指向的值(地址),即指向其它变量;而引用一旦和某个对象绑定后就不能再改变,总是指向最初的对象。
  • 编译:程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名以及变量所对应的地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,指针可以改变,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改
  • 类型:引用其值为地址的数据元素,Java封装了的地址,可以转成字符串查看,长度可以不必关心,C指针是一个装地址的变量,长度一般是计算机字长,可以认为是个int
  • 内存占用:引用声明时没有实体,不占空间,C指针如果声明后会用到才会赋值,如果用不到不会分配内存
  • 内存溢出:java引用不用主动回收。C指针是容易产生内存溢出的,所以程序员需小心使用、及时回收。
  • 初始化:java的引用初始值为 null。c的指针是int,如不初始化指针,那它的值就不是固定的了。


本质:java中的引用和C++中的指针本质上都是想通过一个叫做引用或者指针的东西,找到要操作的目标(变量对象等),方便在程序里操作。所不同的是JAVA的办法更安全,方便些,但没有了C++的灵活,高效。


以上内容仅供学习使用,如有错误,感谢指正!

相关文章
|
1月前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
67 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
5月前
|
Java
【Java基础】输入输出流(IO流)
Java基础、输入输出流、IO流、流的概念、输入输出流的类层次结构图、使用 InputStream 和 OutputStream流类、使用 Reader 和 Writer 流类
168 2
|
2月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
5月前
|
安全 Java
|
5月前
|
安全 Java 编译器
杭州 【Java基础知识 11】java泛型方法的定义和使用(学习+改进+自己理解,想法) (借鉴-侵-删)
杭州 【Java基础知识 11】java泛型方法的定义和使用(学习+改进+自己理解,想法) (借鉴-侵-删)
41 1
|
5月前
|
搜索推荐 算法 Java
【Java基础】 几种简单的算法排序
几种简单的JAVA算法排序
53 4
|
6月前
|
Java 调度 开发者
Java中的多线程编程:基础知识与实践
【5月更文挑战第29天】 在现代软件开发中,多线程编程是一个不可忽视的领域。特别是在Java这种广泛使用的编程语言中,掌握多线程的概念和技术对于开发高效、响应迅速的应用程序至关重要。本文将深入探讨Java多线程的核心概念、实现机制以及常见问题的解决方案。我们将从基础出发,逐步揭示如何通过多线程提升程序性能,并且讨论并发编程中的挑战和解决策略。文章的目的是为开发者提供一个清晰的多线程编程指南,帮助他们在实际项目中有效地应用这些知识。
39 3
|
6月前
|
Java 开发者
Java中的多线程编程:基础知识与实践
【5月更文挑战第26天】 在现代计算机科学中,多线程编程是一个复杂且强大的概念。Java作为一种广泛使用的编程语言,其对多线程的支持使得开发者能够创建出更为复杂、高效和响应迅速的应用程序。本文将深入探讨Java中的多线程编程,从基础概念到实际编程技巧,为读者提供全面的理解和实践指导。
|
6月前
|
监控 IDE Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第30天】 在现代软件开发中,多线程编程是提高应用程序性能和响应能力的关键。特别是在Java这样的多平台、多用途的编程语言中,掌握多线程技术至关重要。本文旨在为读者提供有关Java多线程编程的全面介绍,从基本概念到高级应用技巧,帮助初学者和有经验的开发者理解并有效使用多线程。文章不仅涉及基础理论,还包括代码示例和最佳实践,确保读者能够将知识应用于实际项目中。
|
6月前
|
Java 程序员 调度
Java中的多线程编程:基础知识与实践
【5月更文挑战第28天】 在现代计算机科学中,多线程编程是一个重要的概念,它允许程序同时执行多个任务。在Java中,多线程编程是通过创建并管理线程来实现的。本文将介绍Java中多线程编程的基础知识,包括线程的概念、创建和控制线程的方法,以及线程同步和通信的相关技术。通过阅读本文,读者将对Java中的多线程编程有一个全面的了解,并能在实际编程中应用这些知识。