为了更好的总结Java面试中的系统知识结构,本文根据以下资料整理学习笔记。
在 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 blockBase 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 反射框架主要提供以下功能:
重点:是运行时而不是编译时
很多人都认为反射在实际的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,重点掌握前三个。
@Deprecatedpublic String toLocaleString() { DateFormat formatter = DateFormat.getDateTimeInstance(); return formatter.format(this);}
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)@Documentedpublic @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<具体类型实参> 的父类。参考资料:
理解编码的关键,是要把字符的概念和字节的概念理解准确。这两个概念容易混淆,我们在此做一下区分:
类型 | 概念描述 | 举例 |
---|---|---|
字符 | 人们使用的记号,抽象意义上的一个符号。 | '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 | √ | × | × | × |
参考资料:
跟上Java8 - 了解lambda
https://zhuanlan.zhihu.com/p/28093333
Java 中字符串对象创建有两种形式,一种为字面量形式,如 String str = "droid";
,另一种就是使用 new 这种标准的构造对象的方法,如 String str = new String("droid");
,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式。然而这两种实现其实存在着一些性能和内存占用的差别。这一切都是源于 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 }}
可以将一个类的定义放在另一个类的定义内部,这就是内部类。
在 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...
在成员内部类中要注意两点:
有这样一种内部类,它是嵌套在方法和作用于内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。
//定义在方法里: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 修饰的内部类我们称之为静态内部类,不过我们更喜欢称之为嵌套内部类。静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围内,但是静态内部类却没有。
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_staticOutClass name :chenssychenssy_innerOuterClass 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;}
final int x = 1;// x = 2; // cannot assign value to final variable 'x'final A y = new A();y.a = 1;
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() 只会在对象内存回收前被调用一次 (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
如果
如果为 false,则程序抛出 AssertionError,并终止执行。
assert
如果
如果为 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 的应用范围很多,主要包括:
每次都读错,美式发音: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.5public class TestDemo { public static void main(String[] args) { Integer m =10; int i = m; }}
上面的代码在 jdk1.4 以后的版本都不会报错,它实现了自动拆装箱的功能,如果是 jdk1.4,就得这样写了
// jdk 1.4public 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); // falseInteger 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 | (A^B)得到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
public static void main(String[] args) { if((23==23)||(100/0==0)){ System.out.println("运算没有问题。"); }else{ System.out.println("没有报错"); } }// 此时输出“运算没有问题”。若将||改为|则会报错。
在编程语言中,字面量(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 */ }}
以下为 Object 中的通用方法
public final native Class<?> getClass()public native int hashCode()public boolean equals(Object obj)protected native Object clone() throws CloneNotSupportedExceptionpublic String toString()public final native void notify()public final native void notifyAll()public final native void wait(long timeout) throws InterruptedExceptionpublic final void wait(long timeout, int nanos) throws InterruptedExceptionpublic final void wait() throws InterruptedExceptionprotected void finalize() throws Throwable {} // JVM内存回收之finalize()方法
1. equals() 与 == 的区别
Integer x = new Integer(1);Integer y = new Integer(1);System.out.println(x.equals(y)); // trueSystem.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)); // trueHashSet<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
,编译器会自动进行这个优化。
@Overridepublic 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。
参考资料:
联系客服