本文主要包含 Java 核心基础知识,主要根据以下部分进行节选,选择了个人认为在面试中最为核心的部分。
主要内容:基本概念、面向对象、关键字、基本数据类型与运算、字符串与数组、异常处理、Object 通用方法
在 Java 语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建象。
初始化一般遵循3个原则:
加载顺序
实例
class Base {
// 1.父类静态代码块
static {
System.out.println("Base static block!");
}
// 3.父类非静态代码块
{
System.out.println("Base block");
}
// 4.父类构造器
public Base() {
System.out.println("Base constructor!");
}
}
public class Derived extends Base {
// 2.子类静态代码块
static{
System.out.println("Derived static block!");
}
// 5.子类非静态代码块
{
System.out.println("Derived block!");
}
// 6.子类构造器
public Derived() {
System.out.println("Derived constructor!");
}
public static void main(String[] args) {
new Derived();
}
}
结果是:
Base static block!
Derived static block!
Base block
Base constructor!
Derived block!
Derived constructor!
参考资料:
首先看一个在知乎上的优秀回答吧:
反射是什么呢?当我们的程序在运行时,需要动态的加载一些类这些类可能之前用不到所以不用加载到 JVM,而是在运行时根据需要才加载,这样的好处对于服务器来说不言而喻。
举个例子我们的项目底层有时是用 mysql,有时用 oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了,假设 com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection 这两个类我们要用,这时候我们的程序就写得比较动态化,通过 Class tc = Class.forName("com.java.dbtest.TestConnection"); 通过类的全类名让 JVM 在服务器中找到并加载这个类,而如果是 Oracle 则传入的参数就变成另一个了。这时候就可以看到反射的好处了,这个动态性就体现出 Java 的特性了!
举多个例子,大家如果接触过 spring,会发现当你配置各种各样的 bean 时,是以配置文件的形式配置的,你需要用到哪些 bean 就配哪些,spring 容器就会根据你的需求去动态加载,你的程序就能健壮地运行。
反射 (Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通过 Class 获取 class 信息称之为反射(Reflection)
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。
程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射框架主要提供以下功能:
1. 在运行时判断任意一个对象所属的类
2. 在运行时构造任意一个类的对象
3. 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用 private 方法)
4. 在运行时调用任意一个对象的方法
重点:是运行时而不是编译时
很多人都认为反射在实际的 Java 开发应用中并不广泛,其实不然。
当我们在使用 IDE (如Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。
反射最重要的用途就是开发各种通用框架
很多框架(比如 Spring )都是配置化的(比如通过 XML 文件配置 JavaBean,Action 之类的),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。
.class
属性Class clazz1 = Person.class;
System.out.println(clazz1.getName());
getClass();
Person p = new Person();
Class clazz3 = p.getClass();
System.out.println(clazz3.getName());
forName
静态方法public static Class<?> forName(String className)
// 在JDBC开发中常用此方法加载数据库驱动:
Class.forName(driver);
ClassLoader classLoader = this.getClass().getClassLoader();
Class clazz5 = classLoader.loadClass(className);
System.out.println(clazz5.getName());
参考资料:
Annontation 是 Java5 开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation 像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation
包中。
简单来说:注解其实就是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相对应的处理。
传统的方式,我们是通过配置文件 .xml
来告诉类是如何运行的。
有了注解技术以后,我们就可以通过注解告诉类如何运行
例如:我们以前编写 Servlet 的时候,需要在 web.xml 文件配置具体的信息。我们使用了注解以后,可以直接在 Servlet 源代码上,增加注解...Servlet 就被配置到 Tomcat 上了。也就是说,注解可以给类、方法上注入信息。
明显地可以看出,这样是非常直观的,并且 Servlet 规范是推崇这种配置方式的。
在 java.lang 包下存在着5个基本的 Annotation,重点掌握前三个。
@Override 重写注解
@Deprecated 过时注解
@Deprecated
public String toLocaleString() {
DateFormat formatter = DateFormat.getDateTimeInstance();
return formatter.format(this);
}
@SuppressWarnings 抑制编译器警告注解
@SafeVarargs Java 7“堆污染”警告
@FunctionalInterface 用来指定该接口是函数式接口
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 水果名称注解
*/
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitName {
String value() default "";
}
通俗的讲,泛型就是操作类型的 占位符,即:假设占位符为 T,那么此次声明的数据结构操作的数据类型为T类型。
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?答案是可以使用 Java 泛型。
使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
public class GenericMethodTest
{
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
public static void main( String args[] )
{
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "整型数组元素为:" );
printArray( intArray ); // 传递一个整型数组
System.out.println( "\n双精度型数组元素为:" );
printArray( doubleArray ); // 传递一个双精度型数组
System.out.println( "\n字符型数组元素为:" );
printArray( charArray ); // 传递一个字符型数组
}
}
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("菜鸟教程"));
System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}
类型通配符一般是使用 ?
代替具体的类型参数。例如 List<?>
在逻辑上是 List<String>
,List<Integer>
等所有 List<具体类型实参> 的父类。
类型通配符上限通过形如 List 来定义,如此定义就是通配符泛型值接受 Number 及其下层子类类型。
类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其三层父类类型,如 Objec 类型的实例。
参考资料:
理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:
类型 | 概念描述 | 举例 |
---|---|---|
字符 | 人们使用的记号,抽象意义上的一个符号。 | '1', '中', 'a', '$', '¥', …… |
字节 | 计算机中存储数据的单元,一个 8 位的二进制数,是一个很具体的存储空间。 | 0x01, 0x45, 0xFA, …… |
ANSI 字符串 | 在内存中,如果“字符”是以 ANSI 编码形式存在的,一个字符可能使用一个字节或多个字节来表示,那么我们称这种字符串为 ANSI 字符串或者多字节字符串。 | "中文123" (占7字节) |
UNICODE 字符串 | 在内存中,如果“字符”是以在 UNICODE 中的序号存在的,那么我们称这种字符串为 UNICODE 字符串或者宽字节字符串。 | L"中文123" (占10字节) |
字节与字符区别
它们完全不是一个位面的概念,所以两者之间没有“区别”这个说法。不同编码里,字符和字节的对应关系不同:
类型 | 概念描述 |
---|---|
ASCII | 一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为 8 位二进制数,换算为十进制。最小值 0,最大值 255。 |
UTF-8 | 一个英文字符等于一个字节,一个中文(含繁体)等于三个字节 |
Unicode | 一个英文等于两个字节,一个中文(含繁体)等于两个字节。符号:英文标点占一个字节,中文标点占两个字节。举例:英文句号“.”占 1 个字节的大小,中文句号“。”占 2 个字节的大小。 |
UTF-16 | 一个英文字母字符或一个汉字字符存储都需要 2 个字节(Unicode扩展区的一些汉字存储需要4个字节) |
UTF-32 | 世界上任何字符的存储都需要 4 个字节 |
参考资料:
Java 面向对象的基本思想之一是封装细节并且公开接口。Java 语言采用访问控制修饰符来控制类及类的方法和变量的访问权限,从而向使用者暴露接口,但隐藏实现细节。访问控制分为四种级别:
修饰符 | 当前类 | 同 包 | 子 类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
参考资料:
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
lambda 表达式的语法格式如下:
(parameters) -> expression
或
(parameters) -> { statements; }
以下是 lambda 表达式的重要特征:
Lambda 表达式的简单例子:
// 1. 不需要参数,返回值为 5
() -> 5
// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x
// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y
// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)
在 Java8Tester.java 文件输入以下代码:
public class Java8Tester {
public static void main(String args[]){
Java8Tester tester = new Java8Tester();
// 类型声明
MathOperation addition = (int a, int b) -> a + b;
// 不用类型声明
MathOperation subtraction = (a, b) -> a - b;
// 大括号中的返回语句
MathOperation multiplication = (int a, int b) -> { return a * b; };
// 没有大括号及返回语句
MathOperation division = (int a, int b) -> a / b;
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 不用括号
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 用括号
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}
执行以上脚本,输出结果为:
$ javac Java8Tester.java
$ java Java8Tester
10 + 5 = 15
10 - 5 = 5
10 x 5 = 50
10 / 5 = 2
Hello Runoob
Hello Google
使用 Lambda 表达式需要注意以下两点:
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
在 Java8Tester.java 文件输入以下代码:
public class Java8Tester {
final static String salutation = "Hello! ";
public static void main(String args[]){
GreetingService greetService1 = message ->
System.out.println(salutation + message);
greetService1.sayMessage("Runoob");
}
interface GreetingService {
void sayMessage(String message);
}
}
执行以上脚本,输出结果为:
$ javac Java8Tester.java
$ java Java8Tester
Hello! Runoob
我们也可以直接在 lambda 表达式中访问外层的局部变量:
public class Java8Tester {
public static void main(String args[]) {
final int num = 1;
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2); // 输出结果为 3
}
public interface Converter<T1, T2> {
void convert(int i);
}
}
lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)
int num = 1;
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;
//报错信息:Local variable num defined in an enclosing scope must be final or effectively
final
在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
String first = "";
//编译会出错
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());
Java 中字符串对象创建有两种形式,一种为字面量形式,如 String str = "abc";
,另一种就是使用 new 这种标准的构造对象的方法,如 String str = new String("abc");
,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式。然而这两种实现其实存在着一些性能和内存占用的差别。这一切都是源于 JVM 为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池。
工作原理
当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。
public class Test {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
// 以上两个局部变量都存在了常量池中
System.out.println(s1 == s2); // true
// new出来的对象不会放到常量池中,内存地址是不同的
String s3 = new String();
String s4 = new String();
/**
* 字符串的比较不可以使用双等号,这样会比较内存地址
* 字符串比较应当用equals,可见String重写了equals
*/
System.out.println(s3 == s4); // false
System.out.println(s3.equals(s4)); // true
}
}
我们使用工具编写的字母加符号的代码,是我们能看懂的高级语言,计算机无法直接理解,计算机需要先对我们编写的代码翻译成计算机语言,才能执行我们编写的程序。
将高级语言翻译成计算机语言有编译,解释两种方式。两种方式只是翻译的时间不同。
1. 编译型语言
编译型语言写得程序在执行之前,需要借助一个程序,将高级语言编写的程序翻译成计算机能懂的机器语言,然后,这个机器语言就能直接执行了,也就是我们常见的(exe文件)。
2. 解释型语言
解释型语言的程序不需要编译,节省了一道工序,不过解释型的语言在运行的时候需要翻译,每个语句都是执行的时候才翻译,对比编译型语言,效率比较低。通俗来讲,就是借助一个程序,且这个程序能试图理解编写的代码,然后按照编写的代码中的要求执行。
3. 脚本语言
脚本语言也是一种解释型语言,又被称为扩建的语言,或者动态语言不需要编译,可以直接使用,由解释器来负责解释。
脚本语言一般都是以文本形式存在,类似于一种命令。
4. 通俗理解编译型语言和解释型语言
同行讨论编译型语言和解释型语言的时候,这么说过,编译型语言相当于做一桌子菜再吃,解释型语言就是吃火锅。解释型的语言执行效率低,类似火锅需要一边煮一边吃。
六个基本原则(参考《设计模式之禅》)
单一职责(Single Responsibility Principle 简称 SRP):一个类应该仅有一个引起它变化的原因。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。
里氏替换(Liskov Substitution Principle 简称 LSP):任何时候子类型能够替换掉它们的父类型。子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。
依赖倒置(Dependence Inversion Principle 简称 DIP):要依赖于抽象,不要依赖于具体类。要做到依赖倒置,应该做到:①高层模块不应该依赖底层模块,二者都应该依赖于抽象;②抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
接口隔离(Interface Segregation Principle 简称 ISP):不应该强迫客户依赖于他们不用的方法 。接口要小而专,绝不能大而全。臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。
最少知识原则(Least Knowledge Principle 简称 LKP):只和你的朋友谈话。迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。
开闭原则(Open Closed Principle 简称 OCP):软件实体应当对扩展开放,对修改关闭。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱。
其他原则
项目中用到的原则
可以将一个类的定义放在另一个类的定义内部,这就是内部类。
在 Java 中内部类主要分为成员内部类、局部内部类、匿名内部类、静态内部类
成员内部类也是最普通的内部类,它是外围类的一个成员,所以他是可以无限制的访问外围类的所有成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。
public class OuterClass {
private String str;
public void outerDisplay(){
System.out.println("outerClass...");
}
public class InnerClass{
public void innerDisplay(){
str = "chenssy..."; //使用外围内的属性
System.out.println(str);
outerDisplay(); //使用外围内的方法
}
}
// 推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时
public InnerClass getInnerClass(){
return new InnerClass();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.getInnerClass();
inner.innerDisplay();
}
}
--------------------
chenssy...
outerClass...
在成员内部类中要注意两点:
成员内部类中不能存在static
方法, 但是可以存在static
域, 前提是需要使用final
关键字进行修饰.
成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。
有这样一种内部类,它是嵌套在方法和作用于内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。
//定义在方法里:
public class Parcel5 {
public Destionation destionation(String str){
class PDestionation implements Destionation{
private String label;
private PDestionation(String whereTo){
label = whereTo;
}
public String readLabel(){
return label;
}
}
return new PDestionation(str);
}
public static void main(String[] args) {
Parcel5 parcel5 = new Parcel5();
Destionation d = parcel5.destionation("chenssy");
}
}
//定义在作用域内:
public class Parcel6 {
private void internalTracking(boolean b){
if(b){
class TrackingSlip{
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip(){
return id;
}
}
TrackingSlip ts = new TrackingSlip("chenssy");
String string = ts.getSlip();
}
}
public void track(){
internalTracking(true);
}
public static void main(String[] args) {
Parcel6 parcel6 = new Parcel6();
parcel6.track();
}
}
匿名内部类也就是没有名字的内部类。正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写。但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口
实例1:不使用匿名内部类来实现抽象方法
abstract class Person {
public abstract void eat();
}
class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}
public class Demo {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}
运行结果:eat something
可以看到,我们用 Child 继承了 Person 类,然后实现了 Child 的一个实例,将其向上转型为 Person 类的引用
但是,如果此处的 Child 类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?
这个时候就引入了匿名内部类
实例2:匿名内部类的基本实现
abstract class Person {
public abstract void eat();
}
public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
运行结果:eat something
可以看到,我们直接将抽象类 Person 中的方法在大括号中实现了,这样便可以省略一个类的书写,并且,匿名内部类还能用于接口上。
实例3:在接口上使用匿名内部类
interface Person {
public void eat();
}
public class Demo {
public static void main(String[] args) {
Person p = new Person() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}
运行结果:eat something
由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现
最常用的情况就是在多线程的实现上,因为要实现多线程必须继承 Thread 类或是继承 Runnable 接口
实例4:Thread类的匿名内部类实现
public class Demo {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
t.start();
}
}
运行结果:1 2 3 4 5
实例5:Runnable接口的匿名内部类实现
public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
Thread t = new Thread(r);
t.start();
}
}
运行结果:1 2 3 4 5
关键字 static 中提到 static 可以修饰成员变量、方法、代码块,其他它还可以修饰内部类,使用 static 修饰的内部类我们称之为静态内部类,不过我们更喜欢称之为嵌套内部类。静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。
它的创建是不需要依赖于外围类的。
它不能使用任何外围类的非 static 成员变量和方法。
public class OuterClass {
private String sex;
public static String name = "chenssy";
// 静态内部类
static class InnerClass1{
// 在静态内部类中可以存在静态成员
public static String _name1 = "chenssy_static";
public void display(){
// 静态内部类只能访问外围类的静态成员变量和方法
// 不能访问外围类的非静态成员变量和方法
System.out.println("OutClass name :" + name);
}
}
// 非静态内部类
class InnerClass2{
// 非静态内部类中不能存在静态成员
public String _name2 = "chenssy_inner";
// 非静态内部类中可以调用外围类的任何成员,不管是静态的还是非静态的
public void display(){
System.out.println("OuterClass name:" + name);
}
}
// 外围类方法
public void display(){
// 外围类访问静态内部类:内部类
System.out.println(InnerClass1._name1);
// 静态内部类 可以直接创建实例不需要依赖于外围类
new InnerClass1().display();
// 非静态内部的创建需要依赖于外围类
OuterClass.InnerClass2 inner2 = new OuterClass().new InnerClass2();
// 方位非静态内部类的成员需要使用非静态内部类的实例
System.out.println(inner2._name2);
inner2.display();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.display();
}
}
----------------
Output:
chenssy_static
OutClass name :chenssy
chenssy_inner
OuterClass name:chenssy
组合:各部件之间没什么关系,只需要组合即可。例如组装电脑,需要 new CPU(),new RAM(),new Disk()
public class Computer {
public Computer() {
CPU cpu=new CPU();
RAM ram=new RAM();
Disk disk=new Disk();
}
}
class CPU{ }
class RAM{ }
class Disk{ }
继承:子类需要具有父类的功能,各子类之间有所差异。例如 Shape 类作为父类,子类有 Rectangle,CirCle,Triangle……代码不写了,大家都经常用。
代理:飞机控制类,我不想暴露太多飞机控制的功能,只需部分前进左右转的控制(而不需要暴露发射导弹功能)。通过在代理类中 new 一个飞机控制对象,然后在方法中添加飞机控制类的各个需要暴露的功能。
public class PlaneDelegation{
private PlaneControl planeControl; //private外部不可访问
// 飞行员权限代理类,普通飞行员不可以开火
PlaneDelegation(){
planeControl = new PlaneControl();
}
public void speed(){
planeControl.speed();
}
public void left(){
planeControl.left();
}
public void right(){
planeControl.right();
}
}
final class PlaneControl {// final表示不可继承,控制器都能继承那还得了
protected void speed() {}
protected void fire() {}
protected void left() {}
protected void right() {}
}
说明:
构造函数是函数的一种特殊形式。特殊在哪里?构造函数中不需要定义返回类型(void 是无需返回值的意思,请注意区分两者),且构造函数的名称与所在的类名完全一致,其余的与函数的特性相同,可以带有参数列表,可以存在函数的重载现象。
一般用来初始化一些成员变量,当要生成一个类的对象(实例)的时候就会调用类的构造函数。如果不显示声明类的构造方法,会自动生成一个默认的不带参数的空的构造函数。
public class Demo{
private int num=0;
//无参构造函数
Demo()
{
System.out.println("constractor_run");
}
//有参构造函数
Demo(int num)
{
System.out.println("constractor_args_run");
}
//普通成员函数
public void demoFunction()
{
System.out.println("function_run");
}
}
在这里要说明一点,如果在类中我们不声明构造函数,JVM 会帮我们默认生成一个空参数的构造函数;如果在类中我们声明了带参数列表的构造函数,JVM 就不会帮我们默认生成一个空参数的构造函数,我们想要使用空参数的构造函数就必须自己去显式的声明一个空参的构造函数。
构造函数的作用
通过开头的介绍,构造函数的轮廓已经渐渐清晰,那么为什么会有构造函数呢?构造函数有什么作用?构造函数是面向对象编程思想所需求的,它的主要作用有以下两个:
父类引用能指向子类对象,子类引用不能指向父类对象;
向上造型
父类引用指向子类对象,例如:
Father f1 = new Son();
向下造型
把指向子类对象的父类引用赋给子类引用,需要强制转换,例如:
Father f1 = new Son();
Son s1 = (Son)f1;
但有运行出错的情况:
Father f2 = new Father();
Son s2 = (Son)f2; //编译无错但运行会出现错误
在不确定父类引用是否指向子类对象时,可以用 instanceof 来判断:
if(f3 instanceof Son){
Son s3 = (Son)f3;
}
1. 数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
对于基本类型,final 使数值不变;
对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1;
// x = 2; // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
2. 方法
3. 类
1. 静态变量
静态变量在内存中只存在一份,只在类初始化时赋值一次。
- 静态变量:类所有的实例都共享静态变量,可以直接通过类名来访问它;
- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
public class A {
private int x; // 实例变量
public static int y; // 静态变量
}
注意:不能再成员函数内部定义static变量。
2. 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。
3. 静态语句块
静态语句块在类初始化时运行一次。
4. 静态内部类
内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非静态的变量和方法。
5. 静态导包
import static com.xxx.ClassName.*
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
6. 变量赋值顺序
静态变量的赋值和静态语句块的运行优先于实例变量的赋值和普通语句块的运行,静态变量的赋值和静态语句块的运行哪个先执行取决于它们在代码中的顺序。
public static String staticField = "静态变量";
static {
System.out.println("静态语句块");
}
public String field = "实例变量";
{
System.out.println("普通语句块");
}
最后才运行构造函数
public InitialOrderTest() {
System.out.println("构造函数");
}
存在继承的情况下,初始化顺序为:
跳出当前循环;但是如果是嵌套循环,则只能跳出当前的这一层循环,只有逐层 break 才能跳出所有循环。
for (int i = 0; i < 10; i++) {
// 在执行i==6时强制终止循环,i==6不会被执行
if (i == 6)
break;
System.out.println(i);
}
输出结果为0 1 2 3 4 5 ;6以后的都不会输出
终止当前循环,但是不跳出循环(在循环中 continue 后面的语句是不会执行了),继续往下根据循环条件执行循环。
for (int i = 0; i < 10; i++) {
// i==6不会被执行,而是被中断了
if (i == 6)
continue;
System.out.println(i);
}
输出结果为0 1 2 3 4 5 7 8 9; 只有6没有输出
特别注意:返回值为 void 的方法,从某个判断中跳出,必须用 return。
final 用于声明属性、方法和类,分别表示属性不可变、方法不可覆盖和类不可被继承。
在异常处理的时候,提供 finally 块来执行任何的清除操作。如果抛出一个异常,那么相匹配的 catch 字句就会执行,然后控制就会进入 finally 块,前提是有 finally 块。例如:数据库连接关闭操作上
finally 作为异常处理的一部分,它只能用在 try/catch 语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。(×)(这句话其实存在一定的问题,还没有深入了解,欢迎大家在 issue 中提出自己的见解)
finalize() 是 Object 中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它 finalize() 方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。要明白这个问题,先看一下虚拟机是如何判断一个对象该死的。
可以覆盖此方法来实现对其他资源的回收,例如关闭文件。
Java 采用可达性分析算法来判定一个对象是否死期已到。Java中以一系列 "GC Roots" 对象作为起点,如果一个对象的引用链可以最终追溯到 "GC Roots" 对象,那就天下太平。
否则如果只是A对象引用B,B对象又引用A,A B引用链均未能达到 "GC Roots" 的话,那它俩将会被虚拟机宣判符合死亡条件,具有被垃圾回收器回收的资格。
上面提到了判断死亡的依据,但被判断死亡后,还有生还的机会。
如何自我救赎:
对象覆写了 finalize() 方法(这样在被判死后才会调用此方法,才有机会做最后的救赎);
在 finalize() 方法中重新引用到 "GC Roots" 链上(如把当前对象的引用 this 赋值给某对象的类变量/成员变量,重新建立可达的引用).
需要注意:
finalize() 只会在对象内存回收前被调用一次 (The finalize method is never invoked more than once by a Java virtual machine for any given object. )
finalize() 的调用具有不确定性,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了)。
虽然以上以对象救赎举例,但 finalize() 的作用往往被认为是用来做最后的资源回收。 基于在自我救赎中的表现来看,此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。
综上:finalize() 方法并没有什么鸟用。
至于为什么会存在一个鸡肋的方法:书中说 “它不是 C/C++ 中的析构函数,而是 Java 刚诞生时为了使 C/C++ 程序员更容易接受它所做出的一个妥协”。
参考资料:
断言(assert)作为一种软件调试的方法,提供了一种在代码中进行正确性检查的机制,目前很多开发语言都支持这种机制。
在实现中,assertion 就是在程序中的一条语句,它对一个 boolean 表达式进行检查,一个正确程序必须保证这个 boolean 表达式的值为 true;如果该值为 false,说明程序已经处于不正确的状态下,系统将给出警告并且退出。一般来说,assertion 用于保证程序最基本、关键的正确性。assertion 检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion 检查通常是关闭的。下面简单介绍一下 Java 中 assertion 的实现。
在语法上,为了支持 assertion,Java 增加了一个关键字 assert。它包括两种表达式,分别如下:
assert <boolean表达式>
如果 <boolean表达式> 为 true,则程序继续执行。
如果为 false,则程序抛出 AssertionError,并终止执行。
assert <boolean表达式> : <错误信息表达式>
如果 <boolean表达式> 为 true,则程序继续执行。
如果为 false,则程序抛出 java.lang.AssertionError,并输入<错误信息表达式>。
public static void main(String[] args) {
System.out.println("123");
int a = 0;
int b = 1;
assert a == b; //需显示开启,默认为不开启状态
assert a == b : "执行失败!";
System.out.println("1234");
}
assert 的应用范围很多,主要包括:
断言是编程术语,表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为 false
时中断当前操作的话,可以使用断言。单元测试必须使用断言(Junit/JunitX)。
断言 | 描述 |
---|---|
void assertEquals([String message], expected value, actual value) | 断言两个值相等。值可能是类型有 int, short, long, byte, char or java.lang.Object. 第一个参数是一个可选的字符串消息 |
void assertTrue([String message], boolean condition) | 断言一个条件为真 |
void assertFalse([String message],boolean condition) | 断言一个条件为假 |
void assertNotNull([String message], java.lang.Object object) | 断言一个对象不为空(null) |
void assertNull([String message], java.lang.Object object) | 断言一个对象为空(null) |
void assertSame([String message], java.lang.Object expected, java.lang.Object actual) | 断言,两个对象引用相同的对象 |
void assertNotSame([String message], java.lang.Object unexpected, java.lang.Object actual) | 断言,两个对象不是引用同一个对象 |
void assertArrayEquals([String message], expectedArray, resultArray) | 断言预期数组和结果数组相等。数组的类型可能是 int, long, short, char, byte or java.lang.Object. |
每次都读错,美式发音:volatile /'vɑlətl/ adj. [化学] 挥发性的;不稳定的;爆炸性的;反复无常的
volatile 是一个类型修饰符(type specifier),它是被设计用来修饰被不同线程访问和修改的变量。在使用 volatile 修饰成员变量后,所有线程在任何时间所看到变量的值都是相同的。此外,使用 volatile 会组织编译器对代码的优化,因此会降低程序的执行效率。所以,除非迫不得已,否则,能不使用 volatile 就尽量不要使用 volatile。
参考资料:
instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
public class Main {
public static void main(String[] args) {
Object testObject = new ArrayList();
displayObjectClass(testObject);
}
public static void displayObjectClass(Object o) {
if (o instanceof Vector)
System.out.println("对象是 java.util.Vector 类的实例");
else if (o instanceof ArrayList)
System.out.println("对象是 java.util.ArrayList 类的实例");
else
System.out.println("对象是 " + o.getClass() + " 类的实例");
}
}
strictfp,即 strict float point (精确浮点)。
strictfp 关键字可应用于类、接口或方法。使用 strictfp 关键字声明一个方法时,该方法中所有的 float 和 double 表达式都严格遵守 FP-strict 的限制,符合 IEEE-754 规范。当对一个类或接口使用 strictfp 关键字时,该类中的所有代码,包括嵌套类型中的初始设定值和代码,都将严格地进行计算。严格约束意味着所有表达式的结果都必须是 IEEE 754 算法对操作数预期的结果,以单精度和双精度格式表示。
如果你想让你的浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,可以用关键字strictfp.
transient 英 /'trænzɪənt/ adj. 短暂的;路过的 n. 瞬变现象;过往旅客;候鸟
我们都知道一个对象只要实现了 Serilizable 接口,这个对象就可以被序列化,Java 的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了 Serilizable 接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上 transient 关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
总之,Java 的 transient 关键字为我们提供了便利,你只需要实现 Serilizable 接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
参考资料:
native(即 JNI,Java Native Interface),凡是一种语言,都希望是纯。比如解决某一个方案都喜欢就单单这个语言来写即可。Java 平台有个用户和本地 C 代码进行互操作的 API,称为 Java Native Interface (Java本地接口)。
参考资料:
类型 | 存储 | 取值范围 | 默认值 | 包装类 |
---|---|---|---|---|
整数型 | ||||
byte | 8 | 最大存储数据量是 255,最小 -27,最大 27-1, [-128~127] |
(byte) 0 | Byte |
short | 16 | 最大数据存储量是 65536,[-215,215-1], [-32768,32767],±3万 |
(short) 0 | Short |
int | 32 | 最大数据存储容量是 231-1, [-231,231-1],±21亿,[ -2147483648, 2147483647] |
0 | Integer |
long | 64 | 最大数据存储容量是 264-1, [-263,263-1], ±922亿亿(±(922+16个零)) |
0L | Long |
浮点型 | ||||
float | 32 | 数据范围在 3.4e-45~1.4e38,直接赋值时必须在数字后加上 f 或 F | 0.0f | Float |
double | 64 | 数据范围在 4.9e-324~1.8e308,赋值时可以加 d 或 D 也可以不加 | 0.0d | Double |
布尔型 | ||||
boolean | 1 | true / flase | false | Boolean |
字符型 | ||||
char | 16 | 存储 Unicode 码,用单引号赋值 | '\u0000' (null) | Character |
jdk5.0
提供的新特特性,它可以自动实现类型的转换// jdk 1.5
public class TestDemo {
public static void main(String[] args) {
Integer m =10;
int i = m;
}
}
上面的代码在 jdk1.4 以后的版本都不会报错,它实现了自动拆装箱的功能,如果是 jdk1.4,就得这样写了
// jdk 1.4
public class TestDemo {
public static void main(String[] args) {
Integer b = new Integer(210);
int c = b.intValue();
}
}
new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。
// valueOf 源码实现
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
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;
}
Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些:
因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
参考资料:
i++ 是在程序执行完毕后进行自增,而 ++i 是在程序开始执行前进行自增。
i++ 的操作分三步
三个阶段:内存到寄存器,寄存器自增,写回内存(这三个阶段中间都可以被中断分离开)
所以 i++ 不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致 i 的值不正确自增
在多核的机器上,CPU 在读取内存 i 时也会可能发生同时读取到同一值,这就导致两次自增,实际只增加了一次。
i++ 和 ++i 都不是原子操作
原子性:指的是一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程打断。
JMM 三大特性:原子性,可见性,有序性。详情请阅读 Github 仓库:Java 并发编程 一文。
Java 定义了位运算符,应用于整数类型 (int),长整型 (long),短整型 (short),字符型 (char),和字节型 (byte)等类型。
下表列出了位运算符的基本运算,假设整数变量A的值为60和变量B的值为13
A(60):0011 1100
B(13):0000 1101
操作符 | 名称 | 描述 | 例子 |
---|---|---|---|
& | 与 | 如果相对应位都是 1,则结果为 1,否则为 0 | (A&B)得到 12,即 0000 1100 |
| | 或 | 如果相对应位都是 0,则结果为 0,否则为 1 | (A|B)得到 61,即 0011 1101 |
异或如果相对应位值相同,则结果为 0,否则为 1(AB)得到49,即 0011 0001 | |||
〜 | 非 | 按位取反运算符翻转操作数的每一位,即 0 变成 1,1 变成 0 | (〜A)得到-61,即1100 0011 |
<< | 左移 | (左移一位乘2)按位左移运算符。左操作数按位左移右操作数指定的位数。左移 n 位表示原来的值乘 2n | A << 2得到240,即 1111 0000 |
>> | (右移一位除2)有符号右移,按位右移运算符。左操作数按位右移右操作数指定的位数 | A >> 2得到15即 1111 | |
>>> | 无符号右移 | 无符号右移,按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充 | A>>>2得到15即0000 1111 |
一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号,正数为 0,负数为 1。
比如,十进制中的数 +3 ,计算机字长为 8 位,转换成二进制就是 00000011。如果是 -3 ,就是 10000011 。那么,这里的 00000011 和 10000011 就是机器数。
因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位 1 代表负,其真正数值是 -3 而不是形式值 131(10000011 转换成十进制等于 131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
例:0000 0001 的真值 = +000 0001 = +1,1000 0001 的真值 = –000 0001 = –1
原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如如果是 8 位二进制:
[+1]原 = 0000 0001
[-1]原 = 1000 0001
第一位是符号位。因为第一位是符号位,所以 8 位二进制数的取值范围就是:[1111 1111 , 0111 1111],即:[-127 , 127]
原码是人脑最容易理解和计算的表示方式
反码的表示方法是:
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原= [11111110]反
可见如果一个反码表示的是负数, 人脑无法直观的看出来它的数值. 通常要将其转换成原码再计算。
补码的表示方法是:
[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
对于负数,补码表示方式也是人脑无法直观看出其数值的。 通常也需要转换成原码在计算其数值。
参考资料:
如果给定整数 a 和 b,用以下三行代码即可交换 a 和b 的值
a = a ^ b;
b = a ^ b;
a = a ^ b;
说明:位运算的题目基本上都带有靠经验积累才会做的特征,也就是准备阶段需要做足够多的题,面试时才会有良好的感觉。
#include <stdio.h>
int add(int a, int b)
{
int c = a & b;
int r = a ^ b;
if(c == 0){
return r;
}
else{
return add(r, c << 1);
}
}
int main(int argn, char *argv[])
{
printf("sum = %d\n", add(-10000, 56789));
return 0;
}
(1)&& 和 & 都是表示与,区别是 && 只要第一个条件不满足,后面条件就不再判断。而 & 要对所有的条件都进行判断。
// 例如:
public static void main(String[] args) {
if((23!=23) && (100/0==0)){
System.out.println("运算没有问题。");
}else{
System.out.println("没有报错");
}
}
// 输出的是“没有报错”。而将 && 改为 & 就会如下错误:
// Exception in thread "main" java.lang.ArithmeticException: / by zero
原因:
&&时判断第一个条件为 false,后面的 100/0==0 这个条件就没有进行判断。
& 时要对所有的条件进行判断,所以会对后面的条件进行判断,所以会报错。
(2)|| 和 | 都是表示 “或”,区别是 || 只要满足第一个条件,后面的条件就不再判断,而 | 要对所有的条件进行判断。 看下面的程序:
public static void main(String[] args) {
if((23==23)||(100/0==0)){
System.out.println("运算没有问题。");
}else{
System.out.println("没有报错");
}
}
// 此时输出“运算没有问题”。若将||改为|则会报错。
String、StringBuffer、StringBuilder
String 不变性的理解
String 有重写 Object 的 hashcode 和 toString吗?
当 equals 方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相对等的两个对象必须有相同的 hashCode
重写 equals 不重写 hashcode 会出现什么问题
在编程语言中,字面量(literal)指的是在源代码中直接表示的一个固定的值。
八进制是用在整数字面量之前添加 “0” 来表示的。
十六进制用在整数字面量之前添加 “0x” 或者 “0X” 来表示的
Java 7 中新增了二进制:用在整数字面量之前添加 “0b” 或者 “0B” 来表示的。
在数值字面量中使用下划线
在 Java7 中,数值字面量,不管是整数还是浮点数都允许在数字之间插入任意多个下划线。并且不会对数值产生影响,目的是方便阅读,规则只能在数字之间使用。
public class BinaryIntegralLiteral {
public static void main(String[] args) {
System.out.println(0b010101);
System.out.println(0B010101);
System.out.println(0x15A);
System.out.println(0X15A);
System.out.println(077);
System.out.println(5_000);
/**
* 输出结果
* 21
* 21
* 346
* 346
* 63
* 5000
*/
}
}
Throwable 是 Java 语言中所有错误和异常的超类(万物即可抛)。它有两个子类:Error、Exception。
异常种类
常见异常的基类(Exception)
常见的异常
以下为 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 native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {} // JVM内存回收之finalize()方法
1. equals() 与 == 的区别
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
2. 等价关系
(一)自反性
x.equals(x); // true
(二)对称性
x.equals(y) == y.equals(x); // true
(三)传递性
if (x.equals(y) && y.equals(z))
x.equals(z); // true;
(四)一致性
多次调用 equals() 方法结果不变
x.equals(y) == x.equals(y); // true
(五)与 null 的比较
对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
x.euqals(null); // false;
3. 实现
public class EqualExample {
private int x;
private int y;
private int z;
public EqualExample(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EqualExample that = (EqualExample) o;
if (x != that.x) return false;
if (y != that.y) return false;
return z == that.z;
}
}
hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。
下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。
EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size()); // 2
理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法:31\*x == (x<<5)-x
,编译器会自动进行这个优化。
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
public class ToStringExample {
private int number;
public ToStringExample(int number) {
this.number = number;
}
}
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
ToStringExample@4554617c
1. cloneable
clone() 是 Object 的 protect 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
public class CloneExample {
private int a;
private int b;
}
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
重写 clone() 得到以下实现:
public class CloneExample {
private int a;
private int b;
@Override
protected CloneExample clone() throws CloneNotSupportedException {
return (CloneExample)super.clone();
}
}
CloneExample e1 = new CloneExample();
try {
CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
java.lang.CloneNotSupportedException: CloneTest
以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。
public class CloneExample implements Cloneable {
private int a;
private int b;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
参考资料: