主要内容
1. Java基本功
2. Java面向对象
3. Java核心技术
1. Java基本功
1.1 Java基础概念与常识
1.1.1 JDK和JRE
JDK是Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
JRE是Java运行时环境,它是运行已编译Java程序所需的所有内容的集合,包括Java虚拟机(JVM),Java类库,Java命令和其他的一些基础构件,但是它不能用于创建新程序。
1.1.2 Java和C++的区别
相同点:
- 都是面向对象,支持封装、继承和多态
不同点:
- Java没有指针直接访问内存,程序内存更安全
- Java类是单继承的,C++支持多继承;java接口可以多继承
- Java有自动内存管理机制,不需要程序员手动释放无用内存
- 在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。
1.1.3 什么是Java程序的主类,应用程序和小程序的主类有何不同
Java程序的主类是含main方法的类。 主方法的修饰符必须是public static void的,但是主类的修饰符不一定是public的。
小程序的主类必须要求是public修饰的。
1.1.4 Java应用程序与小程序之间的区别
应用程序从主线程启动,也就是从main()方法启动;
applet小程序(Java编写的小应用程序,jsp中的java脚本)没有main()方法,主要是嵌在浏览器页面上运行(调用init()或者run())来启动。
1.1.5 为什么说Java语言“编译与解释并存”
编译型:编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;
解释型:解释型语言是指解释器对源程序逐行解析成特定平台的机器码并立即执行。
Java预先编译,生成字节码(.class 文件)-> 编译型;
字节码(.class 文件)需要用Java解释器来解释执行 -> 解释型;
Therefore, Java编译与解释并存。
1.2 Java语法
1.2.1 Java泛型,类型擦除,常用的通配符
Java泛型:泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
类型擦除:Java的泛型是伪泛型,因为Java在编译期间所有的泛型信息都会被擦掉。
泛型的使用方式:泛型类;泛型接口;泛型方法
泛型类
/**
* 泛型类
* 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
*/
//此处T可以随便写成任意标识,常见的如T,E,K,V等形式的参数常用于表示泛型
//但是在实例化泛型类时,必须指定T的具体类型
public class GenericClass {
private T key;
public GenericClass(T key){
this.key = key;
System.out.println(key);
}
public T getKey(){
return key;
}
public static void main(String[] args) {
GenericClass genericClassInteger = new GenericClass<>(1234);
}
}
泛型方法
public class GenericMethod {
public static 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[] intArr = {1,2,3};
String[] stringArr = {"hello", " ","world"};
Character[] characterArr = {'c','h','e','e','r'};
printArray(intArr);
printArray(stringArr);
printArray(characterArr);
}
}
泛型接口
/**
* 泛型接口
* @param
*/
public interface Generator {
public T method();
}
/**
* 实现泛型接口,不指定类型
* @param
*/
public class GeneratorImpl implements Generator {
@Override
public T method() {
return null;
}
}
/**
* 实现泛型接口,指定类型
*/
public class GeneratorImpl2 implements Generator {
@Override
public String method() {
return "hi~~ cheer";
}
}
常用的通配符:
- ?不确定的java类型
- T (type) 表示具体的一个java类型
- K V (key value) 分别表示java键值对中的键和值
- E (element) 表示Element元素
1.2.2 ==和equals的区别
==:判断两个对象是不是同一个对象,基本数据类型比较的是值, 引用数据类型比较的是内存地址
equals:比较两个对象是否相等,不能用于比较基本数据类型;equals方法存在于Object类之中,Object类是所有类的直接或间接父类。
类没有覆盖equals()方法,等价通过"=="比较两个对象,默认使用Object类的equals()方法。
类覆盖了equals()方法,某些值或内容相等默认两个对象相等。例如String之中的equals()方法就是被重写过的。
补充关于String类型的小知识:当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
1.2.3 hashCode()与equals()
hashCode() 获取哈希码(散列码),实际是一个int型整数。这个哈希码的作用是确定该对象在哈希表中的索引位置,默认对堆上的对象产生独特的值。散列表中存储的时键值对(key-value), 特点是:能根据“键”快速检索对应的值。HashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。
hashCode()存在的意义 提高执行速度;当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。
为什么重写equals()时必须重写hashCode方法
hashCode()和equals()的相关规定: 两对象相等则hashcode一定相等;两个对象相等,equals方法会返回true;两对象hashcode值相等,它们不一定相等
重新equals是希望两个对象在某些值相等的时候就默认相等,hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
为什么两个对象有相同的hashcode值,它们也不一定相等
因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode。-- hash碰撞问题
重写equals和hashCode方法
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* equals() and hashCode(), which are used in various collections
*
* Summary:
* 1. If two objects are equal, they MUST have the same hashcode
* 2.
*/
public class equalAndHashCode {
private static class Person{
private String firstName;
private String lastName;
private int ssn;
public Person(String firstName, String lastName, int ssn) {
this.firstName = firstName;
this.lastName = lastName;
this.ssn = ssn;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getSsn() {
return ssn;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (!(obj instanceof Person)) return false;
Person p = (Person) obj;
return this.firstName.equals(p.getFirstName())
&&this.lastName.equals(p.getLastName())
&&this.ssn == p.getSsn();
}
@Override
public int hashCode() {
return Objects.hash(firstName,lastName,ssn);
}
}
public static void main(String[] args) {
Person p1 = new Person("John","Smith",111111);
Person p2 = new Person("John","Smith",111111);
Person p3 = new Person("Bob","Smith",111111);
System.out.println(p1.equals(p2));
System.out.println(p1.equals(p3));
Set set = new HashSet<>();
set.add(p1);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(set.size());
}
}
1.3 基本数据类型
1.3.1 8种基本类型的包装类和常量池
实现常量池的基本类型的包装类
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。-- 为相应的包装类创建对应类型的缓存数据
两种浮点数类型的包装类Float和Double没有实现常量池技术。
应用场景
- Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
- Integer i1 = new Integer(40);这种情况下会创建新的对象。
代码示例
public class testConstantPool {
public static void main(String[] args) {
//对Integer线程池技术理解
/* Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2); //输出为true,由于缓冲数据池的存在
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22); //输出为false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4); //输出为false, Float Double并没有实现常量池技术*/
//Integer比较丰富的例子
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2)); //true
System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); //true
System.out.println("i1=i4 " + (i1 == i4)); //false
System.out.println("i4=i5 " + (i4 == i5));//false
/**
*解释:
* 语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,+/-/*等运算符都会将数据转化成数值类型来进行操作
* 首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。
* 然后 Integer 对象无法与数值进行直接比较,
* 所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。
*/
System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); //true
System.out.println("40=i5+i6 " + (40 == i5 + i6)); //true
}
}
1.4 方法(函数)
1.4.1 为什么Java中只有值传递
按值调用:表示方法接收的是调用者提供的值
按引用调用:表示方法接收的是调用者提供的变量地址。
一个方法可以改变传递引用所对应的原变量的值,而不能修改传递值调用对应的变量值。
Java程序设计的特点
Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
Java中方法参数的使用情况
一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
/**
* 我们已经知道了一个方法不能修改一个基本数据类型的参数
*/
public class primaryDataTypePara {
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
一个方法可以修改一个对象参数的状态
/**
* 实现一个改变对象参数状态的方法并不是一件难事。
* 理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
*/
public class referencePara {
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
}
一个方法不能让原本的对象参数引用一个新的对象
/**
* 方法并没有改变存储在变量 s1 和 s2 中的对象引用。
* swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,
* 这个方法交换的是这两个拷贝
*/
public class referenceParaSwap {
public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
referenceParaSwap.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
static class Student{
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
1.4.2 重载和重写方法的区别
从发生范围、参数列表、返回类型、异常、访问修饰符和发生阶段来进行说明。
1.4.3 深拷贝 vs 浅拷贝
浅拷贝: 对基本数据类型进行值传递,对引用数据类型进行传递般的拷贝,此为浅拷贝。
深拷贝: 对基本数据类型进行值传递,对引用数据类型,创建一个新对象,并复制其内容,为深拷贝
2. Java面向对象
2.1 类和对象
2.1.1 面向对象和面向过程的区别
面向过程: 大多数面向过程的语言大多数是直接编译成机械码在电脑上执行,并且其他的一些面向过程的脚本语言的性能不一定比Java好。不需要实例化对象。所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。
面向对象: 面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。
2.1.2 在Java定义一个不做事并且没有参数的构造函数的作用
为了子类好:Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。
子类实例化时,默认调用父类的无参构造方法(不管子类的构造器有没有参数,因为子类继承的是父类的属性和方法,只调用父类的无参构造器就可以继承父类的属性和方法,因此不会调用父类的有参构造器),再调用子类的有参/无参构造器。
2.1.3 创建对象所用的运算符;对象实体和对象引用的不同点
创建对象所用的运算符: new
区别点:对象实例存在堆内存中,对象引用存在栈内存中;一个对象引用可以指向0个或1个对象,一个对象可以有n个引用指向它。
堆和栈的区别:
内存大小:堆非常大;栈很小
工作效率:堆相对比较慢,栈就会快一些
2.1.4 调用子类构造方法之前会先调用父类无参的构造方法,目的?
因为子类继承的是父类的属性和方法,只调用父类的无参构造器就可以继承父类的属性和方法,因此调用父类的无参构造器是为了帮助子类做初始化工作。
2.2 面向对象的三大特征
2.2.1 封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
2.2.2 继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
2.2.3 多态
表示一个对象具有多种状态。具体表现为父类的引用指向子类的实例。
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 对象类型不可变,引用类型可变
- 方法具有多态性,属性不具有多态性
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定
- 多态不能调用“只在子类存在但在父类不存在”的方法
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法
2.3 修饰符
2.3.1 常见关键字总结: static, final, this, super
final关键字(不可修改;修饰类、方法和变量)
final关键字修饰的特点:
- final修饰的类不能被继承,final类中的所有成员方法都会被隐式的指定为final方法
- final修饰的方法不能被重写
- final修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
使用final方法的原因:
- 把方法锁定,以防任何继承类修改它的含义
- 效率,在早期的Java实现版本中,会将final方法转为内嵌调用。类中所有的private方法都隐式地指定为final。
static关键字
static关键字使用场景
- 修饰成员变量和成员方法
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享(不属于单个实例),可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,调用格式:类名.静态变量名 类名.静态方法名()。静态变量 存放在 Java 内存区域的方法区。
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
/**
* static修饰的成员属于类的例子
*/
public class StaticBean {
private String name;
//静态变量
static int age;
public StaticBean(String name) {
this.name = name;
}
//静态方法
static void SayHello() {
System.out.println("Hello i am java");
}
@Override
public String toString() {
return "StaticBean{"+
"name=" + name + ",age=" + age +
"}";
}
}
public class StaticDemo {
public static void main(String[] args) {
StaticBean staticBean = new StaticBean("1");
StaticBean staticBean2 = new StaticBean("2");
StaticBean staticBean3 = new StaticBean("3");
StaticBean staticBean4 = new StaticBean("4");
//static变量是属于整个类的,所以赋值就使得该类的所有实例的age有了相应的改变
StaticBean.age = 33;
System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4);
//StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33}
StaticBean.SayHello();//Hello i am java
}
}
- 静态代码块
静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
/**
* static{}静态代码块与{}非静态代码块的执行顺序
*/
public class CodeBlock {
public CodeBlock(){
System.out.println("默认构造方法!--");
}
/**
* 每次实例化都会执行
*/
//非静态代码块
{
System.out.println("非静态代码块");
}
/**
* 该类无论创建多少对象,静态代码块只执行一次
*/
//静态代码块
static {
System.out.print("静态代码块!--");
}
private static void test() {
System.out.print("静态方法中的内容! --");
{
System.out.print("静态方法中的代码块!--");
}
System.out.println();
}
public static void main(String[] args) {
CodeBlock.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!--
CodeBlock test = new CodeBlock();
}
}
- 静态内部类(static修饰类的花只能修饰内部类)
静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
public class Super {
private int number;
void showNumber(){
System.out.println(this);
System.out.println("number = " + number);
}
static void showNum(){
System.out.println(1111);
}
/**
* 测试静态内部类,静态内部类不可以引用外部的非
*/
static class Test{
void outerMethod(){
showNum();
}
}
/**
* 测试非静态内部类
*/
public class Test2{
void outerMethod(){
showNumber();
}
}
static class Sub extends Super{
/**
* 继承就意味着子类拥有父类所有属性和方法(构造方法除外)。
* static方法同样能被继承,
* 只是在某些情况下(比如类向上转型时)调用时与非static方法出现差异。
*/
void bar(){
showNumber();
super.number=10;
super.showNumber();
System.out.println(this);
System.out.println(super.number);
}
}
public static void main(String[] args) {
Sub sub = new Sub();
Sub sub1 = new Sub();
((Super)sub).showNumber();
sub.bar();
sub1.bar();
Super super1 = new Super();
super1.showNumber();
/**
* 测试非静态内部类
*/
Test2 test = new Super().new Test2();
test.outerMethod();
/**
* 测试静态内部类
*/
Test test1 = new Super.Test();
test1.outerMethod();
}
}
- 静态导包(用来导入类中的静态资源,1.5之后的新特性)
import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
import static java.lang.Math.*;
public class staticImportPackage {
public static void main(String[] args) {
System.out.println(max(1,4));
}
}
static method / non-static method
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
- 静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
静态代码块与非静态代码块
- 相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。
- 不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行
- 非静态代码块与构造函数的区别: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,非静态代码块中定义的是不同对象共性的初始化内容。
this和super关键字
this关键字
- this关键字用于引用类的当前实例。可以访问当前实例的变量和方法。
super关键字
- super关键字用于从子类访问父类的变量和方法。
使用this和super要注意的问题
- 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
- this、super不能用在static方法中。(this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西。)
2.4 接口和抽象类
2.4.1 接口和抽象类的区别
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。
- 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
- 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
- 总结一下 jdk7~jdk9 Java 中接口概念的变化: 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现; jdk8 的时候接口可以有默认方法和静态方法功能; Jdk 9 在接口中引入了私有方法和私有静态方法.
/**
* java8的时候接口有默认方法和静态方法功能
*/
public interface Test {
public void generalInterfaceMethod();
default void defaultMethod(){
System.out.println("test default method");
}
public static void test(){
System.out.println("hahahahha");
}
public static void main(String[] args) {
Test.test();
}
}
2.5 其它重要的知识点
2.5.1 String StringBuffer和StringBuilder的区别, String为什么不可变
String StringBuffer和StringBuilder
- String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。
- 而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
线程安全性
- String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
- 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险
对三者使用的总结
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
2.5.2 Object类的常见方法总结
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法
public final native Class> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
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 { }//实例被垃圾回收器回收的时候触发的操作
2.5.3 Java序列化如果有些字段不想进行序列化,怎么办
使用transient关键字,阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
序列化和反序列化: Java 序列化是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject() 方法可以实现序列化。
Java 反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
2.5.4 获取用键盘输入常用的两种方法
- 通过Scanner
- 通过BufferReader
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class KeyboardInput {
public static void main(String[] args) throws IOException {
/*Scanner input = new Scanner(System.in);
String s = input.nextLine();
System.out.println(s);
input.close();*/
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
System.out.println(s);
}
}
3. Java核心技术
3.1 集合
3.1.1 Collections工具类和Arrays工具类常见方法总结
Collections工具类和Arrays工具类都是对原本数组直接进行操作
Collections
- 排序操作
void reverse(List list)://反转
void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
void sort(List list) //按自然排序的升序排序
void shuffle(List list) //随机排序
void swap(List list, int i , int j) //交换两个索引位置的元素
//定制排序的用法
Collections.sort(arrayList, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
具体的实例:
/**
* 排序的常用方法
*/
/* ArrayList arrayList = new ArrayList<>();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始数组:");
System.out.println(arrayList);
// void reverse(List list):反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse(arrayList):");
System.out.println(arrayList);
//void rotate(List list, int distance)//旋转。当distance为正数时,
//将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
Collections.rotate(arrayList,4);
System.out.println("Collections.rotate(arrayList, 4):");
System.out.println(arrayList);
//void sort(List list),按自然排序的升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort(arrayList):");
System.out.println(arrayList);
//void shuffle(List list),随机排序
Collections.shuffle(arrayList);
System.out.println("Collections.shuffle(arrayList):");
System.out.println(arrayList);
//void swap(List list, int i , int j),交换两个索引位置的元素
Collections.swap(arrayList, 2, 5);
System.out.println("Collections.swap(arrayList, 2, 5):");
System.out.println(arrayList);
//定制排序的用法
Collections.sort(arrayList, new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("定制排序后: ");
System.out.println(arrayList);*/
- 查找/替换操作
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
boolean replaceAll(List list, Object oldVal, Object newVal) //用新元素替换旧元素
int indexOfSubList(List list, List target) //统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
Collections.binarySearch(arrayList, 7) //
具体实例:
/**
* 查找,替换操作
*/
/* ArrayList arrayList = new ArrayList();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
ArrayList arrayList2 = new ArrayList();
arrayList2.add(-3);
arrayList2.add(-5);
arrayList2.add(7);
System.out.println("原始数组:");
System.out.println(arrayList);
//int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
System.out.println("Collections.max(arrayList):");
System.out.println(Collections.max(arrayList));
System.out.println("Collections.min(arrayList):");
System.out.println(Collections.min(arrayList));
//collections是对原本的数组直接进行操作
//boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
System.out.println("Collections.replaceAll(arrayList, 3, -3):");
Collections.replaceAll(arrayList, 3, -3);
System.out.println(arrayList);
//int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,
//找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
System.out.println("Collections.indexOfSubList(arrayList, arrayList2):");
System.out.println(Collections.indexOfSubList(arrayList, arrayList2));
System.out.println("Collections.binarySearch(arrayList, 7):");
// 对List进行二分查找,返回索引,List必须是有序的
Collections.sort(arrayList);
System.out.println(Collections.binarySearch(arrayList, 7));*/
- 同步控制
- 提供了多个synchronizedXXX()方法
Collections提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。 - 设置不可变集合
emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合可以是:List,Set,Map。
singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。
unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。
上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。
- 提供了多个synchronizedXXX()方法
/**
* collections设置不可变集合
*/
ArrayList arrayList = new ArrayList();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
HashSet integers1 = new HashSet<>();
integers1.add(1);
integers1.add(3);
integers1.add(2);
Map scores = new HashMap();
scores.put("语文" , 80);
scores.put("Java" , 82);
//Collections.emptyXXX();创建一个空的、不可改变的XXX对象
List
Arrays
- 排序: sort();查找: binarySearch();比较: equals();填充: fill();转列表: asList();转字符串: toString();复制: copyOf()
具体实例
import java.util.Arrays;
import java.util.List;
/**
* arrays类的常见方法
*/
public class arraysMethods {
public static void main(String[] args) {
//1. 排序: sort()
// *************排序 sort****************
int a[] = { 1, 3, 2, 7, 6, 5, 4, 9 };
// sort(int[] a)方法按照数字顺序排列指定的数组。
Arrays.sort(a);
System.out.println("Arrays.sort(a):");
for (int i : a) {
System.out.print(i);
}
// 换行
System.out.println();
// sort(int[] a,int fromIndex,int toIndex)按升序排列数组的指定范围
int b[] = { 1, 3, 2, 7, 6, 5, 4, 9 };
Arrays.sort(b, 2, 6);
System.out.println("Arrays.sort(b, 2, 6):");
for (int i : b) {
System.out.print(i);
}
// 换行
System.out.println();
int c[] = { 1, 3, 2, 7, 6, 5, 4, 9 };
// parallelSort(int[] a) 按照数字顺序排列指定的数组(并行的)。同sort方法一样也有按范围的排序
Arrays.parallelSort(c);
System.out.println("Arrays.parallelSort(c):");
for (int i : c) {
System.out.print(i);
}
// 换行
System.out.println();
// parallelSort给字符数组排序,sort也可以
char d[] = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };
Arrays.parallelSort(d);
System.out.println("Arrays.parallelSort(d):");
for (char d2 : d) {
System.out.print(d2);
}
// 换行
System.out.println();
//Collections可以对字符串进行排序
String[] strs = { "abcdehg", "abcdefg", "abcdeag" };
Arrays.sort(strs);
System.out.println(Arrays.toString(strs));//[abcdeag, abcdefg, abcdehg]
/**
* 2.查找 binarySearch
*/
// *************查找 binarySearch()****************
char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };
// 排序后再进行二分查找,否则找不到
Arrays.sort(e);
System.out.println("Arrays.sort(e)" + Arrays.toString(e));
System.out.println("Arrays.binarySearch(e, 'c'):");
int s = Arrays.binarySearch(e, 'c');
System.out.println("字符c在数组的位置:" + s);
/**
* 3. 比较: equals()
*/
// *************比较 equals****************
// *************比较 equals****************
char[] e1 = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };
char[] f = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };
/*
* 元素数量相同,并且相同位置的元素相同。 另外,如果两个数组引用都是null,则它们被认为是相等的 。
*/
// 输出true
System.out.println("Arrays.equals(e, f):" + Arrays.equals(e1, f));
/**
* 4.填充: fill(), 批量进行初始化
*/
// *************填充fill(批量初始化)****************
int[] g = { 1, 2, 3, 3, 3, 3, 6, 6, 6 };
// 数组中所有元素重新分配值
Arrays.fill(g, 3);
System.out.println("Arrays.fill(g, 3):");
// 输出结果:333333333
for (int i : g) {
System.out.print(i);
}
// 换行
System.out.println();
int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, };
// 数组中指定范围元素重新分配值
Arrays.fill(h, 0, 2, 9);
System.out.println("Arrays.fill(h, 0, 2, 9);:");
// 输出结果:993333666
for (int i : h) {
System.out.print(i);
}
System.out.println();
/**
* 转列表 asList()
*/
// *************转列表 asList()****************
/*
* 返回由指定数组支持的固定大小的列表。
* (将返回的列表更改为“写入数组”。)该方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()相结合。
* 返回的列表是可序列化的,并实现RandomAccess。
* 此方法还提供了一种方便的方式来创建一个初始化为包含几个元素的固定大小的列表如下:
*/
List stooges = Arrays.asList("Larry", "Moe", "Curly");
System.out.println(stooges);
/**
* 转字符串 toString()
*/
// *************转字符串 toString()****************
/*
* 返回指定数组的内容的字符串表示形式。
*/
char[] k = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' };
System.out.println(Arrays.toString(k));// [a, f, b, c, e, A, C, B]
/**
* 复制 copyOf()
*/
// *************复制 copy****************
// copyOf 方法实现数组复制,h为数组,6为复制的长度
int[] h2 = { 1, 2, 3, 3, 3, 3, 6, 6, 6, };
int i[] = Arrays.copyOf(h2, 6);
System.out.println("Arrays.copyOf(h2, 6);:");
// 输出结果:123333
for (int j : i) {
System.out.print(j);
}
// 换行
System.out.println();
// copyOfRange将指定数组的指定范围复制到新数组中
int j[] = Arrays.copyOfRange(h2, 6, 11);
System.out.println("Arrays.copyOfRange(h2, 6, 11):");
// 输出结果66600(h数组只有9个元素这里是从索引6到索引11复制所以不足的就为0)
for (int j2 : j) {
System.out.print(j2);
}
// 换行
System.out.println();
}
}
3.2 异常
3.2.1 Java异常类层次结构图
所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable: 有两个重要的子类:Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别:异常能被程序本身处理,错误是无法处理的。
- error:
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。 - exception:
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 异常由 Java 虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
3.2.2 Throwable类常用方法
- public string getMessage():返回异常发生时的简要描述
- public string toString():返回异常发生时的详细信息
- public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
- public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息
3.2.3 try-catch-finally
- try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch块: 用于处理 try 捕获到的异常。
- finally块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
在以下四种特殊情况下,finally块不会被执行- 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行
- 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行
- 程序所在的线程死亡
- 关闭CPU
注意
当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。
3.2.4 使用 try-with-resources
来代替try-catch-finally
- 面对必须要关闭的资源,我们总是应该优先使用try-with-resources而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources语句让我们更容易编写必须要关闭的资源的代码。 Java 中类似于InputStream、OutputStream 、Scanner 、PrintWriter等的资源都需要我们调用close()方法来手动关闭。使用try-with-resources语句就无需手动关闭资源了。
- 当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。
通过使用分号分隔,可以在try-with-resources块中声明多个资源。
示例
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
3.3 多线程
3.3.1 简述线程、程序、进程的基本概念;以及他们之间的关系
线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,线程也被称为轻量级进程。
程序
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
进程和线程的区别
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
3.3.2 线程的基本状态
Java线程运行的生命周期
Java线程在运行的生命周期中的指定时刻只能处于6种状态的其中一个状态。
线程之间的状态变迁
线程在生命周期中并不是固定处于某一种状态,随着代码执行在不同状态之间切换,状态变迁图如下图所示:
-
初始和运行状态
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。
操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
等待、超时等待、阻塞和终止状态
当线程执行 wait()方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
3.4 文件与I/O流
3.4.1 Java中的IO流分为几种
Java中IO流分为几种
- 按照流的流向分,可以分为输入流和输出流
- 按照操作单元分,可以分为字节流和字符流
- 按照流的角色分为节点流和处理流
节点流:可以从或向一个特定的地方(节点)读写数据,如FileReader。
处理流:是对一个已经存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。
Java IO流的类是从下面的4个抽象类的基类中派生出来的。
- InputStream/Reader: 所有输入流的基类,前者是字节输入流,后者是字符输入流
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图
按操作对象分类结构图
既然有了字节流,为什么还要有字符流
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
BIO, NIO和AIO的区别