数据类型
基本类型
- byte/8
- char/16
- short/16
- int/32
- float/32
- long/64
- double/64
- boolean/~
包装类型
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
1 | Integer x = 2; // 装箱 调用了 Integer.valueOf(2) |
缓存池(!!!重要)
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象;
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
1 | Integer x = new Integer(123); |
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。 编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values (byte取值范围内的全部值 -128~127)
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。
在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=
[String](https://cyc2018.github.io/CS-Notes/#/notes/Java 基础?id=二、string)
String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
在 Java 8 中,String 内部使用 ** final byte[] 数组存储数据**。
内部实现:
1 | public final class String |
value数组被声明为final,这意味着value数组初始化之后就不能在引用其他数组,并且String内部没有改变value数组的方法,因此可以保证String不可变
String Pool
字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。
1 | String s1 = new String("aaa"); |
不可变的好处
1. 可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool(否则可变的话,某个修改操作将导致所有引用该对象的值都发生改变)。
String, StringBuffer and StringBuilder
1. 可变性
- String 不可变
- StringBuffer 和 StringBuilder 可变
2. 线程安全
- String 不可变,因此是线程安全的
- StringBuilder 不是线程安全的
- StringBuffer 是线程安全的,内部使用 synchronized 进行同步 (几乎所有方法都用了synchronized关键字进行加锁)
运算
参数传递
!!!! Java 的参数是以值传递的形式传入方法中,而不是引用传递。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。
1 | public class PassByValueExample { |
如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容 !!!
1 | class PassByValueExample { |
float与double
Java 不能隐式执行向下转型,因为这会使得精度降低。
1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。
1 | // float f = 1.1; |
1.1f 字面量才是 float 类型。
1 | float f = 1.1f; |
隐式类型转换
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
1 | short s1 = 1; |
但是使用 += 或者 ++ 运算符可以执行隐式类型转换。
1 | s1 += 1; |
上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
1 | s1 = (short) (s1 + 1); |
switch
从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。但是不支持Long !!!!
继承
权限修饰符
Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示default包级可见
可以对类或类中的成员(字段以及方法)加上访问修饰符。
重写与重载
1. 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里式替换原则,重写有以下三个限制:
- 子类方法的访问权限必须大于等于父类方法; ==( 权限>= )==
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。 ==( 返回类型相容 )==
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。 (抛出异常相容)
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中:
子类方法访问权限为 public,大于父类的 protected。
子类的返回类型为 ArrayList,是父类返回类型 List 的子类。
子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。
1
2
3
4
5
6
7
8
9//父类
protected List<Integer> func() throws Throwable {
return new ArrayList<>();
}
//子类重写的
public ArrayList<Integer> func() throws Exception {
return new ArrayList<>();
}2. 重载(Overload)
重载只关心的是参数是否相同,而不关心返回值类型 !!!!
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
equals()
Object类中默认实现是比较两个对象的地址引用是否相等
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
hashCode()
hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
!!! 所以hashCode是equals() 方法使用的前提
所以重写equals方法一定要重写hashCode方法, 一般多用于hash环境下
toString()
Object类原生实现: 类名+对象的hash值
clone()
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
Object类原生实现: 这个是一个protected方法, 必须要重写才能调用, 并且其原生实现是直接clone的对象的地址属于浅拷贝
浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象。
深拷贝
拷贝对象和原始对象的引用类型引用不同对象。
super
- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,(子类不用super()显式调用的情况下) 一般是自动调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super 函数。
- 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
关键字
static
1. 静态变量
- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
1 | public class A { |
2. 静态方法
只能访问所属类的静态字段和静态方法( 这些属于类所有的, 而不是对象所有),方法中不能有 this 和 super 关键字。
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
1 | public abstract class A { |
3. 静态语句块
静态语句块在类初始化时仅运行一次。
1 | public class A { |
4. 静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要。
静态内部类不能访问外部类的非静态的变量和方法。
1 | public class OuterClass { |
5. 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
1 | import static com.xxx.ClassName.* |
6. 初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
1 | public static String staticField = "静态变量";Copy to clipboardErrorCopied |
最后才是构造函数的初始化。
1 | public InitialOrderTest() { |
存在继承的情况下,初始化顺序为:
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
反射
==每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。==
类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver")
这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
- Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
- Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
- Constructor :可以用 Constructor 的 newInstance() 创建新的对象。
反射的优点:
- 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。
- 类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
- 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
反射的缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(暴露了源码文件: 比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
抽象类与接口
1. 抽象类
抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
2. 接口
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。
接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected (因为接口必须被实现)。
接口的字段默认都是 static 和 final 的。
3. 比较
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
4. 使用选择
使用接口:
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
- 需要使用多重继承。
使用抽象类:
- 需要在几个相关的类中共享代码。
- 需要能控制继承来的成员的访问权限,而不是都为 public。
- 需要继承非静态和非常量字段。
在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
异常
Throwable 可以用来表示任何可以作为异常抛出的类,是所有异常的基类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
- 受检异常 :需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;
- 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。

泛型
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类:
1 | /** |
泛型接口:
1 | /** |
泛型方法:
1 | /** |
泛型面试题:
Java中的泛型是什么 ? 使用泛型的好处是什么?
那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
Java的泛型是如何工作的 ? 什么是类型擦除 ?
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List
在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。 Array中可以用泛型吗?
不可以, Array事实上并不支持泛型, 建议用List来代替Array,list可以提供编译期间的类型安全保证
可以把List
传递给一个接受List 不可以, 会导致编译错误, 因为List
编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
1
2
3public V put(K key, V value) {
return cache.put(key, value);
}
注解
注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。 (存了类结构文件的值)
自定义注解类编写的一些规则:
\1. Annotation 型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.
\2. 参数成员只能用public 或默认(default) 这两个访问权修饰
\3. 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
\4. 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法
\5. 注解也可以没有定义成员,,不过这样注解就没啥用了
PS:自定义注解需要使用到元注解
基本容器
List, Set, Queue 都继承自Collection接口
List
优点 | 缺点 | 保存元素的顺序 | 应用 | |
---|---|---|---|---|
ArrayList | 随机访问速度快,内部使用数组实现。 | 迭代,插入和删除元素慢,尤其是当List]尺寸比较大的时候。 | 插入顺序 | 可变长数组 |
LinkedList | 迭代(顺序访问经过优化),插入,删除都很快内部使用双向链表实现 | 随机访问速度慢 | 插入顺序 | 顺序访问, 批量插入删除元素的场合 |
Set
优点 | 保存元素的顺序 | 要求 | |
---|---|---|---|
HashSet | 为快速查找设计 | 散列存储 | 必须定义hashCode()方法 |
LinkedHashSet | 和HashSet一样的查询速度,但是插入要比HashSet慢一些,因为它通过维护链表形式维护元素。 | 使用链表维护元素顺序(插入顺序) | 必须定义hashCode()方法 |
TreeSet | 保存有序的Set,底层通过TreeMap来实现的 | 按照排序顺序维护元素 | 必须实现Comparable接口(包含compareTo方法) |
HashMap, HashTable都继承自Map接口
Queue
特点 | 保存元素的顺序 | |
---|---|---|
LinkedList | LinkedList除了普通List之外,还添加了很多实现<队列,栈,双向队列>三种数据结构的方法。尤其是模拟Queue的时候在两端插入删除元素很快(经过了优化)。 | 插入的顺序 |
PriorityQueue | 按照排序顺序取出元素,所以要求必须实现Comparable接口。 | 排序顺序 |
Map
特点 | 保存元素的顺序 | 要求 | |
---|---|---|---|
HashMap | Map基于散列存储,插入和查询“键值对”的开销是固定的。 | 散列存储 | 存入的键需要具备hashCode()方法,当然,返回的标识不一定要唯一 |
LinkedHashMap | 为了提高速度散列了所有元素,插入查询只比HashMap慢一点点,因为它在维护散列数据结构的同时还要维护链表(插入顺序)。 但是迭代访问的时候更快,因为内部使用链表维护次序。 | 插入顺序 | 同样需要键实现hashCode()方法 |
TreeMap | Map基于红黑树的实现。所以所得的结果是经过排序的。 | 红黑树 | 为了排序,必须实现Comparable接口。 |
特性
Java 各版本的新特性
New highlights in Java SE 8
- Lambda Expressions
- Pipelines and Streams
- Date and Time API
- Default Methods
- Type Annotations
- Nashhorn JavaScript Engine
- Concurrent Accumulators
- Parallel operations
- PermGen Error Removed
New highlights in Java SE 7
- Strings in Switch Statement
- Type Inference for Generic Instance Creation
- Multiple Exception Handling
- Support for Dynamic Languages
- Try with Resources
- Java nio Package
- Binary Literals, Underscore in literals
- Diamond Syntax
Java 与 C++ 的区别
- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
JRE or JDK
- JRE is the JVM program, Java application need to run on JRE.
- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler “javac”