1. 基本概念
1.1 Java语言有哪些优点?
1.2 Java与C++有什么异同?
1.3 为什么需要public static void main(String[] args)这个方法?
1.4 如何实现在main方法执行前输出“Hello World”?
1.5 Java程序初始化的顺序是怎样的?
1.6 Java中的作用域有哪些?
1.7 一个Java文件中是否可以定义多个类?
1.8 什么是构造函数?
1.9 为什么Java中有些接口没有任何方法?
1.10 Java中的clone方法有什么作用?
1.11 什么是反射机制?
1.12 package有什么作用?
1.13 如何实现类似于C语言中函数指针的功能?
2. 面向对象技术
2.1 面向对象与面向过程有什么区别?
2.2 面向对象有哪些特征?
2.3 面向对象的开发方式有什么优点?
2.4 继承的特性?
2.5 组合和继承有什么区别?
2.6 多态的实现机制是什么?
2.7 重载和覆盖有什么区别?
2.8 抽象类与接口有什么异同?
2.9 内部类有哪些?
2.10 如何获取父类的类名?
2.11 this与super有什么区别?
3. 关键字
3.1 变量命名有哪些规则?
3.2 break、continue和return有什么区别?
3.3 final、finally和finalize有什么区别?
3.4 static关键字有哪些作用?
3.5 使用switch时有哪些注意事项?
3.6 volatile有什么作用?
3.7 instanceof有什么作用?
3.8 怎么处理精度问题?
4. 基本类型与运算
4.1 Java提供了哪些基本数据类型?
4.2 什么是不可变类?
4.3 值传递和引用传递有哪些区别?
4.4 不同数据类型的转换有哪些规则?
4.5 运算符优先级是什么?
4.6 如何实现无符号数的右移操作?
4.7 char类型变量是否可以存储一个中文汉字?
5. 字符串与数组
5.1 字符串创建与存储的机制是什么?
5.2 "=="、euqals()和hashCode()有什么区别?
5.3 String、StringBuffer、StringBuilder和StringTokenizer有什么区别?
5.4 Java中数组是不是对象?
5.5 数组的初始化方式有哪些?
5.6 length属性与length()方法有什么区别?
6. 异常处理
6.1 finally块中的代码什么时候被执行?
6.2 异常处理的原理是什么?
7. 输入输出流
7.1 Java IO流的实现机制是什么?
7.2 管理文件和目录的类是什么?
7.3 Java Socket是什么?
7.4 NIO是什么?
7.5 什么是Java序列化?
8. Java平台与内存管理
8.1 为什么说Java是平台独立性语言?
8.2 JVM加载class文件的机制是什么?
8.3 什么是GC?
8.4 Java是否存在内存泄漏问题?
8.5 Java中的堆和栈有什么区别?
9. 容器
9.1 Java Collections框架是什么?
9.2 什么是迭代器?
9.3 ArrayList、Vector和LinkedList有什么区别?
9.4 HashMap、Hashtable、TreeMap和WeakHashMap有什么区别?
9.5 Collection和Collections有什么区别?
10. 多线程
10.1 什么是线程?它与进程有什么区别?为什么要使用多线程?
10.2 同步和异步有什么区别?
10.3 如何实现Java多线程?
10.4 run()方法和start()方法有什么区别?
10.5 多线程同步的实现方法有哪些?
10.6 sleep()方法和wait()方法有什么区别?
10.7 sleep()方法和yield()方法有什么区别?
10.8 终止线程的方法有哪些?
10.9 synchronized与Lock有什么异同?
10.10 什么是守护线程?
10.11 join()方法的作用是什么?
11. Java数据库操作
11.1 如何通过JDBC访问数据库?
11.2 JDBC处理事务采用什么方法?
11.3 Class.forName的作用是什么?
11.4 Statement、PreparedStatement和CallableStatement有什么区别?
11.5 getString()与getObject()方法有什么区别?
main方法是Java程序的入口方法,JVM在运行程序时,会首先查找main方法。
main方法的返回值必须是void,并由static和public关键字修饰。
在Java中,静态块在类被加载时就会调用,且静态块不管顺序如何,都会在main方法执行之前执行。
public class Test {
static {
System.out.println("Hello World1");
}
public static void main(String[] args) {
System.out.println("Hello World3");
}
static {
System.out.println("Hello World2");
}
}
Java程序的初始化一般遵循3个原则(优先级递减):
总结就是:父类static{} -> 子类static{} -> 父类{} -> 父类constructor -> 子类{} -> 子类constructor。
Java中,变量的类型由3种:
另,访问控制符范围:
当前类 | 同包 | 不同包子类 | 不同包非子类 | |
public | 1 | 1 | 1 | 1 |
protected | 1 | 1 | 1 | 0 |
default | 1 | 1 | 0 | 0 |
private | 1 | 0 | 0 | 0 |
一个Java文件中可以定义多个类,但是最多只能有一个类被public修饰,并且这个类的类名与文件名必须相同。
若这个文件中没有public的类,则文件名随便是一个类的名字即可。
当用javac指令编译这个.java文件时,它会为每一个类生成一个对应的.class文件。
构造函数用来在对象实例化时初始化对象的成员变量。
Java中,构造函数具有以下特点:
接口是抽象方法定义的集合,也可以定义一些常量值,是一种特殊的抽象类。
接口中只包含方法的定义,没有方法的实现。
接口中的所有方法都是抽象的,接口中成员的作用域修饰符都是public,接口中常量值默认使用public static final修饰。
Java 8开始,接口中可以定义default方法和static方法。
在Java中,有些接口内部没有声明任何方法,也就是说,实现这些接口的类不需要重写任何方法,这些没有任何方法声明的接口叫做标识接口,仅仅充当一个标识的作用,用来表明实现它的类属于一个特定的类型。
Java在处理基本数据类型时,都是采用值传递,传递的是输入参数的复制。除此之外的其他类型都是引用传递,传递的是对象的一个引用,对象除了在函数调用时是引用传递,在使用"="赋值时也采用引用传递。
当有如下需求:从某个已有的对象A创建出另外一个与A具有相同状态的对象B,并且对B的修改不会影响到A的状态。使用赋值操作是无法达到目的的,这时可以用clone方法。
clone方法是Object类提供的一个方法,作用是返回一个Object对象的复制,返回的是一个新的对象而不是一个引用。
使用clone方法的步骤:
public class User implements Cloneable {
private int age;
private Date birthday;
public User() {
}
public User(int age, Date birthday) {
this.age = age;
this.birthday = birthday;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", birthday=" + birthday +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
User user1 = new User(23, new Date());
User user2 = (User) user1.clone();
System.out.println((user1 == user2) +
"\n" + user1 +
"\n" + user2 +
"\n" + (user1.getBirthday() == user2.getBirthday())
);
}
}
上述代码中的方式是浅复制:被复制对象的所有变量都含有原来对象相同的值,而所有对其他对象的引用仍然指向原来的对象。
深复制:被复制对象的所有变量都含有原来对象相同的值,除去那些引用其他对象的变量,那些引用其他对象的变量将指向被复制的新对象,而不再是原有的那些被引用的对象,如下面的代码:
public class User implements Cloneable {
private int age;
private Date birthday;
public User() {
}
public User(int age, Date birthday) {
this.age = age;
this.birthday = birthday;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", birthday=" + birthday +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.birthday = (Date) this.getBirthday().clone();
return user;
}
public static void main(String[] args) throws CloneNotSupportedException {
User user1 = new User(23, new Date());
User user2 = (User) user1.clone();
System.out.println((user1 == user2) +
"\n" + user1 +
"\n" + user2 +
"\n" + (user1.getBirthday() == user2.getBirthday())
);
}
}
反射机制允许程序在运行时进行自我检查,同时也允许对其内部的成员进行操作。
反射机制的功能有:得到一个对象所属的类、获取一个类的所有成员变量和方法、在运行时创建对象、在运行时调用对象的方法。
public class ReflectTest {
public static void main(String[] args) {
try {
Class c = Class.forName("com.yeta.review.a.Sub");
c = ReflectTest.class;
c = new ReflectTest().getClass();
Base b = (Base) c.newInstance();
b.f();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Base {
public void f() {
System.out.println("Base");
}
}
class Sub extends Base {
@Override
public void f() {
System.out.println("Sub");
}
}
总共有3种方法可以获取到Class类,如上述代码所示:
【注】Java创建对象的方式有几种?
package的宗旨是:把.java文件、.class文件以及其他resource文件有条理地组织,以供使用。
package的作用是:
可以利用接口与类来实现:先定义一个接口,然后在接口中声明要调用的方法,接着实现这个接口,最后把这个实现类的一个对象作为参数传递给调用程序,调用程序通过这个参数来调用指定的函数,从而实现回调函数的功能。
public class FunctionPointerTest {
public static void func(int a, int b, IntCompare cmp) {
if (cmp.compare(a, b) == 1) {
System.out.println(a + " 大于 " + b);
} else if (cmp.compare(a, b) == -1) {
System.out.println(a + " 小于 " + b);
} else {
System.out.println(a + " 等于 " + b);
}
}
public static void main(String[] args) {
func(1, 2, new AnIntCompare());
}
}
interface IntCompare {
int compare(int a, int b);
}
class AnIntCompare implements IntCompare {
@Override
public int compare(int a, int b) {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
}
}
面向对象是把数据及对数据的操作方法放在一起,作为一个相互依存的整体,即对象。对同类对象抽象出其共性,即类,类中的大多数数据,只能被本类的方法进行处理。类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。程序流程由用户在使用中决定。
面向过程是一种以事件为中心的开发方法,就是自顶向下顺序执行,逐步求精,其程序结构是按功能划分为若干个基本模块,这些模块形成一个树状结构,各模块之间的关系也比较简单,在功能上相对独立,每一个模块内部一般都是由顺序、选择和循环3种基本结构组成,其模块化实现的具体方法是使用子程序,而程序流程在写程序时就已经决定。
抽象就是忽略一个主题中与当前目标无关的那些方法,以便更充分地注意与当前目标有关的方面。
封装是指将客观事务抽象成类,每个类对自身的数据和方法实行保护。
继承是一种联接类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。
多态是允许不同类的对象对同一消息作出响应。
其中第2条标红,是因为从内存实现和反射的角度来看,子类是可以继承父类的所有方法和状态的,参考:
https://blog.csdn.net/techgfuture/article/details/39231951
https://blog.csdn.net/zxc_helloworld/article/details/77451195
组合是指在新类里面创建原有类的对象,重复利用已有类的功能,是has-a关系。
继承是允许根据其他类的实现来定义一个类的实现,是is-a关系。
多态表示当同一个操作作用在不同对象时,会有不同的语义,从而会产生不同的结果。
在Java中,多态有2种表现方式:
重载是指同一个类中有多个同名的方法,这些方法有不同的参数,因此在编译期就能确定到底调用哪个方法。
覆盖是指子类可以覆盖父类的方法,在Java中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象,而程序调用的方法在运行期才动态绑定。
public class OverTest {
public static void main(String[] args) {
Father father = new Son(10);
father.f();
father.g();
System.out.println(father.i);
}
}
class Father {
public int i;
public Father() {
g();
}
public Father(int i) {
this.i = i;
}
public void f() {
System.out.println("Father f()");
}
public void g() {
System.out.println("Father g()");
}
}
class Son extends Father {
public int i;
public Son() {
}
public Son(int i) {
this.i = i;
}
@Override
public void f() {
System.out.println("Son f()");
}
@Override
public void g() {
System.out.println("Son g()");
}
}
上述代码中,Father和Son的构造方法提现了重载,Son中覆盖了Father中的两个方法f()和g()。分析下面代码的流程:
Father father = new Son(10);
father.f();
father.g();
System.out.println(father.i);
对象father的静态类型是Father,实际类型是Son。
第一步:执行new Son(10),先调用父类Father的构造方法;
第二步:父类Father的构造方法中调用g()方法,此时应该调用子类Son的g()方法,因为子类Son中覆盖了父类Father的g()方法,在运行期得知了对象father的实际类型是Son;
第三步:调用对象father的f()方法,与第二步同理;
第四步:调用对象father的g()方法,与第二步同理;
第五步:输出对象father的变量i,对象father的静态类型是Father,这是在编译期就确定了的,所以输出的是父类Father中的变量i。
使用重载的注意事项:
使用覆盖的注意事项:
重载和覆盖的区别:
相同点:
不同点:
另:
静态内部类可以不依赖于外部类实例而被实例化,只能访问外部类的静态成员和方法,可以定义静态或非静态的成员和方法。
成员内部类只能在外部类被实例化之后才能被实例化,可以访问外部类所有的成员和方法,不能定义静态的属性和方法。
局部内部类像局部变量一样,不能被public、protected、private以及static修饰,只能访问方法中定义为final类型的局部变量。
匿名内部类不适用关键字class、extends、implements,没有构造方法,必须继承其他类或者实现其他接口,不能定义静态成员、方法、类,遵循所有局部内部类的规则。
public class InnerClassTest {
private int a1;
private static int a2;
private void func1() {};
private static void func2() {};
// 静态内部类
static class StaticInnerClass {
// 可以定义静态或非静态的成员和方法
private int b1;
private static int b2;
public void f1() {
// 只能访问外部类的静态成员和方法
//System.out.println(a1);
System.out.println(a2);
//func1();
func2();
}
public static void f2 () {}
}
// 成员内部类
class MemberInnerClass {
// 不能定义静态的属性和方法
private int b1;
//private static int b2;
public void f1() {
// 可以访问外部类所有的成员和方法
System.out.println(a1);
System.out.println(a2);
func1();
func2();
}
//public static void f2 () {}
}
public void func() {
int c1 = 0;
final int c2 = 0;
// 局部内部类
class LocalInnerClass {
public void f1() {
// Variable 'c1' is accessed from within inner class, needs to be final or effectively final
//c1++;
System.out.println(c1);
System.out.println(c2);
}
}
// 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {}
}).start();
}
public static void main(String[] args) {
// 静态内部类可以不依赖于外部类实例而被实例化
StaticInnerClass sic = new StaticInnerClass();
// 成员内部类只能在外部类被实例化之后才能被实例化
MemberInnerClass mic = new InnerClassTest().new MemberInnerClass();
}
}
public class FatherNameTest extends Thread {
public void printFatherName1() {
System.out.println(super.getClass().getName());
}
public void printFatherName2() {
System.out.println(this.getClass().getSuperclass().getName());
}
public static void main(String[] args) {
FatherNameTest fnt = new FatherNameTest();
fnt.printFatherName1();
fnt.printFatherName2();
}
}
第一个方法获取的是自己的名字的原因是:
getClass()方法在Object类中被定义为final与native,子类不能覆盖该方法,因此this.getClass()和super.getClass()最终都调用的是Object的getClass()方法,该方法返回此Object的运行时类。
在Java中,this用来指向当前实例对象,它的一个非常重要的作用是用来区分对象的成员变量与方法的形参。
super可以用来访问父类的方法或成员变量。
在Java中,变量名、函数名、数组名统称为标识符,标识符只能由字母、数字、下划线和美元符号组成,并且标识符的第一个字符必须是字母或下划线或美元符号,并且标识符也不能包含空白字符。
另,跳出多重循环:
public class BreakTest {
public static void main(String[] args) {
out:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break out;
}
}
}
}
}
static关键字主要有2种作用:
static关键字有4种使用情况:
static变量属于类,在内存中只有一个复制,所有实例都指向同一个内存地址,只要静态变量所在的类被加载,这个静态变量就会被分配空间,因此就可以被使用了。
static方法是类的方法,不需要创建对象就可以被调用,并且不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和方法,因为当static方法被调用时,该类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法。
【注】static实现单例模式
public class StaticTest {
private static StaticTest st = null;
private StaticTest() {
}
public static StaticTest getInstance() {
if (st == null) {
st = new StaticTest();
}
return st;
}
}
JVM在加载类时会执行static代码块,如果有多个static代码块,JVM将按顺序执行,static代码块常用来初始化静态变量,并且static代码块只会被执行一次。
参考2.9。
switch语句用于多分支选择,switch(expr),其中expr只能是一个枚举常量或一个整数表达式(char, byte, short, int, Integer),Java 7开始支持String。
volatile是一个类型修饰符,用来修饰被不同线程访问和修改的变量,系统每次使用这个变量的时候都从对应的内存中提取,而不是缓存,所以所有的线程在任何时候看到的变量的值都是相同的。
volatile不能保证操作的原子性。
volatile会阻止编译器对代码的优化。
判断一个引用类型的变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例。
Java一共提供了8种原始的数据类型,除了这8种基本的数据类型外,其他类型都是引用类型。
数据类型 | 字节长度 | 范围 | 默认值 | 包装类 |
byte | 1 | [-128, 127] | 0 | Byte |
short | 2 | [-32768, 32767] | 0 | Short |
int | 4 | [-2^31, 2^31-1] | 0 | Integer |
long | 8 | (-2^63, 2^63-1) | 0L | Long |
float | 4 | 32位单精度 | 0.0F | Float |
double | 8 | 64位双精度 | 0.0 | Double |
char | 2 | Unicode [0, 65535] | u0000 | Character |
boolean | 1 | true, false | false | Boolean |
不可变类是指当创建了这个类的实例后,就不允许修改它的值了。
在Java中,所有基本类型的包装类都是不可变类,String也是。
要创建一个不可变类需要遵循4个基本原则:
Java设计很多不可变类是因为不可变类具有使用简单、线程安全、节省内存等优点。
Java中,原始数据类型在传递参数时是按值传递,包装类型在传递参数时是按引用传递。
参考以下代码:
public class CallTest {
public static void change(StringBuffer ss1, StringBuffer ss2) {
ss1.append(" World");
ss2 = ss1;
}
public static void main(String[] args) {
Integer a = 1;
Integer b = a;
b++;
System.out.println(a + " & " + b);
StringBuffer s1 = new StringBuffer("Hello");
StringBuffer s2 = new StringBuffer("Hello");
change(s1, s2);
System.out.println(s1 + " & " + s2);
}
}
类型转换的2种类型:
低级数据类型可以自动转换为高级数据类型:byte < short < char < int < long < float < double。
char类型的数据转换为更高级的类型会转换为其对应的ASCII码。
byte、char、short类型的数据在参与运算时会自动转换为int型,但当使用"+="运算时,不会产生类型的转换。
boolean与其他基本类型之间不能相互转换。
从高级数据类型转换为低级数据类型,可能会损失精度。
优先级 | 运算符 |
1 | . () [] |
2 | +(正) -(负) ++ -- ~ ! |
3 | * / % |
4 | +(加) -(减) |
5 | << >> >>> |
6 | < <= > >= instanceof |
7 | == != |
8 | & |
9 | | |
10 | ^ |
11 | && |
12 | || |
13 | ?: |
14 | = += -= *= /= %= &= |= ^= ~= <<= >>= >>>= |
>>是有符号右移运算符,>>>是无符号右移运算符,将对象对应的二进制数右移指定的位数:
public class RightMoveTest {
public static void main(String[] args) {
int i = -4;
System.out.println("有符号右移1位前: " + Integer.toBinaryString(i));
i >>= 1;
System.out.println("有符号右移1位后: " + Integer.toBinaryString(i) + "\n");
i = -4;
System.out.println("无符号右移1位前: " + Integer.toBinaryString(i));
i >>>= 1;
System.out.println("无符号右移1位后: " + Integer.toBinaryString(i) + "\n");
short j = -4;
System.out.println("有符号右移1位前: " + Integer.toBinaryString(j));
j >>= 1;
System.out.println("有符号右移1位后: " + Integer.toBinaryString(j) + "\n");
j = -4;
System.out.println("无符号右移1位前: " + Integer.toBinaryString(j));
j >>>= 1;
System.out.println("无符号右移1位后: " + Integer.toBinaryString(j) + "\n");
}
}
short j = -4;无符号右移1位后结果为什么最高位是1而不是0呢?这是因为-short先转换为int高位补1,无符号右移高位补0,这时候是01111111 11111111 11111111 11111110,之后又转换为short,只取了低位2个字节,这时候将这个结果按int的二进制输出,高位补1,所以就是那个结果。
【注】<<运算符
左移n位表示原来的值乘2的n次方,左移运算没有有符号与无符号之分,都是在低位补0。
System.out.println(3 << 2 == 3 * (2 * 2)); //true
Java中,默认使用Unicode编码方式,每个字符占用2个字节,可以用来存储中文。
【注】只要是使用new就会生成新对象。
例如以下代码:
String s = new String("abc");
上述代码可分为3个部分:
【问】new String("abc")创建了几个对象?
【答】一个或两个,"abc"如果在字符串常量池中存在,则不会创建,如果存在则创建,new String()创建一个。
StringTokenizer st = new StringTokenizer("Hello World");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
【注】执行效率:StringBuilder > StringBuffer > String,但是一般操作的数据量少,应该优先使用String类,如果在多线程下操作大量数据,应该使用StringBuffer,如果在单线程下操作大量数据,应该使用StringBuilder。
数组是指具有相同类型的数据的集合,它们一般具有固定的长度,并且在内存中占据连续的空间。
在Java中,数据不仅有其自己的属性(如length),还有一些方法可以被调用(如clone),所以数组是对象。
int[] a1 = new int[5];
int[] a2 = {1, 2, 3, 4, 5};
int a3[];
a3 = new int[5];
int a4[];
a4 = new int[]{1, 2, 3, 4, 5};
二维数组:
int[][] a1 = {{1, 2}, {3, 4, 5}};
int[][] a2 = new int[2][];
a2[0] = new int[]{1, 2};
a2[1] = new int[]{3, 4, 5};
int a3[][];
int[] a4[];
声明二维数组时,其中[]必须为空,第二维的长度可以不同。
【注】在Java中,数组被创建后会根据数组存放的数据类型初始化成对应的初始值,数组在定义时并不会给数组元素分配存储空间,所以[]为空,在初始化时才必须为之分配空间。
在Java中,finally块的作用就是为了保证无论出现什么情况,里面的代码一定会被执行。
finally块中的代码是在try块或者catch块中的return语句之前执行的,这时候如果在finally块中对return的变量进行修改,如果该变量是基本类型,那么无任何影响,但是如果该变量是引用类型,那么return的是在finally块中改变后的对象。
如果try块或者catch块与finally块中都有return,那么finally块中的return语句将会覆盖别处的return语句。
整个过程就是:程序在执行到return时会首先将返回值存储在一个指定的位置,其次去执行finally块,最终再返回。
finally块中的代码不一定会被执行:
异常是指程序运行时所发生的非正常情况或错误,当程序违反了语义规则时,JVM会将出现的错误表示为一个异常并抛出,这个异常可以在catch块中捕获处理,异常处理的目的就是为了提高程序的安全性和鲁棒性。
【注】Java异常处理用到了多态的概念,如果在异常处理过程中,先捕获了基类,然后再捕获子类,那么捕获子类的代码将永远不会被执行,所以应该先捕获子类,再捕获基类。
在Java中,输入和输出都被称为抽象的流,流可以被看作一组有序的字节集合,即数据在两设备之间的传输。
流的本质是数据传输,根据处理数据类型的不同,流可以分为两大类:
public class IOTest {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
File file = new File("files/IOTest.txt");
File fileCopy = new File("files/IOTestCopy.txt");
if (file.exists()) {
try {
if (!fileCopy.exists()) {
fileCopy.createNewFile();
}
is = new FileInputStream(file);
os = new FileOutputStream(fileCopy);
byte[] b = new byte[1024];
while (is.read(b) != -1) {
String s = new String(b);
System.out.println(s);
os.write(b);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class IOTest {
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
File file = new File("files/IOTest.txt");
File fileCopy = new File("files/IOTestCopy.txt");
if (file.exists()) {
try {
if (!fileCopy.exists()) {
fileCopy.createNewFile();
}
br = new BufferedReader(new FileReader(file));
bw = new BufferedWriter(new FileWriter(fileCopy));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
bw.write(line + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Java通过File来管理文件和目录,通过该类不仅能够查看文件或目录的属性,而且还可以实现对文件或目录的创建、删除和重命名等操作。
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个Socket。
Socket也称为套接字,可以用来实现不同虚拟机或不同计算机之间的通信。
在Java中,Socket可以分为面向连接的Socket通信协议和面向无连接的Socket通信协议。
任何一个Socket都是由IP地址和端口号唯一确定的。
class Server {
public static void main(String[] args) {
BufferedReader br = null;
PrintWriter pw = null;
try {
ServerSocket ss = new ServerSocket(2000);
Socket socket = ss.accept();
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);
String line = br.readLine();
pw.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (pw != null) {
pw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Client {
public static void main(String[] args) {
BufferedReader br = null;
PrintWriter pw = null;
try {
Socket socket = new Socket("localhost", 2000);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);
pw.println("hello");
String line;
while (true) {
line = br.readLine();
if (line != null) {
break;
}
}
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (pw != null) {
pw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在NIO(即非阻塞IO)出现之前,Java是通过传统的Socket来实现基本的网络通信功能的,如果客户端没有对服务端发起连接请求,那么服务端accept就会阻塞,如果连接成功,当数据还没有准备好时,read同样会阻塞,当有多个连接的时候,就需要采用多线程的方式,所以程序的运行效率非常低下,因此引入NIO来解决这个问题。
NIO通过Selector、Channel和Buffer来实现非阻塞的IO操作:
在分布式环境下,当进行远程通信时,无论是何种类型的数据,都会以二进制序列的形式在网络上传送。
序列化是一种将对象以一串字节描述的过程,用于解决在对对象流进行读写操作时所引发的问题。
序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要时把该流读取出来重新构造一个相同的对象。
所有要实现序列化的类都必须实现Serializable接口。
如果一个类能够被序列化,那么它的子类也能够被序列化。
被static、transient(表示临时数据)修饰的成员不能被序列化。
序列化能实现深复制。
public class SerializableTest {
public static void main(String[] args) {
User user = new User("YETA", 23);
File file = new File("files/SerializableTest.txt");
//序列化
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//反序列化
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
user = (User) ois.readObject();
System.out.println(user);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
反序列化是将流转换为对象,在过程中通过serialVersionUID来判断类的兼容性,最好显示地声明它,可以提高程序的运行效率、提高程序不同平台上的兼容性和增强程序各个版本的可兼容性。
外部序列化与序列化的区别在于序列化是内置的API,只需要实现Serializable接口,不需要编写任何代码就可以实现对象的序列化,然而外部序列化Externalizable接口中的方法必须要进行实现。
外部序列化可以作为只序列化部分属性的一种方法,另种方法是用transient修饰对应属性。
平台独立性是指可以在一个平台上编写和编译程序,而在其他平台上运行。保证Java具有平台独立性的机制是字节码和JVM。Java程序被编译后生成字节码,不同的硬件平台上会安装不同的JVM,由JVM来负责把字节码翻译成硬件平台能执行的代码。
JVM解释执行的过程分为3步:
当运行指定程序时,JVM会将编译生成的.class文件(字节码)按照需求和一定的规则加载到内存中,并组织称为一个完成的Java应用程序,这个加载过程由类加载器完成。
类加载的方式有2种:
类加载的步骤有3步:
Java的类加载器:
https://blog.csdn.net/qq_28958301/article/details/83052489#%E5%9B%9B%E3%80%81%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8
在Java中,GC(垃圾回收)的主要作用是回收程序中不再使用的内存。Java提供了垃圾回收器来自动检测对象的作用域,可自动地把不再被使用的存储空间释放掉。
对对象而言,如果没有任何变量去引用它,那么该对象将不可能被程序访问,因此可以认为它是垃圾信息,可以被回收,只要有一个以上的变量引用该对象,该对象就不会被垃圾回收。
几种常用的垃圾回收算法:
https://blog.csdn.net/qq_28958301/article/details/83052489#%E4%B8%89%E3%80%81%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95
内存泄漏是指一个不再被程序使用的对象或变量还在内存中占有存储空间。
一般来讲,内存泄漏有两种情况:一是在堆中申请的空间没有被释放,二是对象已不再被使用,但还仍然在内存中保留着。垃圾回收机制可以有效地解决第一种情况,因此内存泄漏主要是指第二种情况。
引起内存泄漏的原因:
在Java中,基本数据类型的变量和对象的引用变量的内存都分配在栈上,而引用类型的变量的内存分配在堆上或者常量池。
从作用方面来看,堆主要用来存放对象,栈主要用来执行程序。栈的存取速度更快,但栈的大小和生命周期必须是确定的,因此缺乏灵活性。而堆可以在运行时动态地分配内存,生存期不用提前告诉编译器,这也导致了其存取速度缓慢。
Java Collections框架中包含了大量集合接口以及这些接口的实现类和操作它们的算法。
迭代器是一个对象,它的工作是遍历并选择序列中的对象。
public class CollectionsTest {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add("hello");
list.add("world");
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
在使用Iterator遍历容器的过程中,如果对容器进行增加或删除操作,就会改变容器中对象的数量,从而导致抛出异常。如果是删除,解决方法可以是在迭代过程中,将要删除的元素存入一个集合中,迭代完毕直接removeAll(),或者使用迭代器的remove()方法。
ListIterator只存在于List中,支持在迭代期间向List中添加或删除元素,并且可以在List中双向移动。
ArrayList和Vector都是基于Objcet[] array来实现的,查询数据快,增加、删除数据需要移动容器里的元素,所以比较慢,ArrayList的容量每次默认扩充为原来的1.5倍,Vector扩充为原来的2倍,ArrayList是线程不安全的,Vector是线程安全的。
LinkedList是基于双向链表来实现的,查询数据慢,增加、删除数据不需要移动容器里的元素,所以比较块,是线程不安全的。
WeakHashMap | HashMap | TreeMap | Hashtable |
线程不安全 | 线程安全 | ||
key, value都可以为null | key不可以为null, value可以为null | key, value都不可以为null | |
Iterator |
Enumeration | ||
默认大小16,一定是2的指数 | 默认大小11,扩展old*2+1 | ||
将保存的记录根据key排序 | |||
弱引用 | 强饮用 |
将HashMap同步的方式:
Map map = Collections.synchronizedMap(new HashMap<>());
Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,实现该接口的类主要是List和Set,该接口的设计目标是为各种具体的集合提供最大化的统一的操作方式。
Collections是针对集合类的一个包装类,它提供了一系列静态方法以实现对各种集合的搜索、排序、线程安全化等操作,其中大多数防范都是用来处理线性表。Collections类不能实例化,如同一个工具类。
线程是指程序在执行过程中,能够执行程序代码的一个执行单元。
进程是指一段正在执行的程序,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源,但是各个线程拥有自己的栈空间。
为什要使用多线程:
同步就是当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用。
异步就是多个线程各执行各的,不关心其它线程的状态和行为。
继承Thread类,重写run()方法;
实现Runnable接口,实现run()方法;
实现Callable接口,实现call()方法;
Callable可以在任务结束后提供一个返回值,还可以在call()方法中抛出异常,Future的get()会阻塞当前线程。
public class ThreadTest {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
myThread1.start();
Thread myThread2 = new Thread(new MyThread2());
myThread2.start();
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future future = threadPool.submit(new MyThread3());
try {
System.out.println(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread1 extends Thread {
@Override
public void run() {
//
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
//
}
}
class MyThread3 implements Callable {
@Override
public String call() throws Exception {
return null;
}
}
调用start()方法可以启动一个线程,是线程进入就绪状态,等待被调度执行。
调用run()方法会当作一个普通函数来调用,是同步的。
https://blog.csdn.net/qq_28958301/article/details/83146528#%E4%BA%8C%E3%80%81%C2%A0%E5%AF%B9%E8%B1%A1%E5%8F%8A%E5%8F%98%E9%87%8F%E7%9A%84%E5%B9%B6%E5%8F%91%E8%AE%BF%E9%97%AE
sleep()方法是Thread类的方法,线程自己就可以醒过来,而wait()是Object类的方法,需要拥有同一对象锁的其他线程进行唤醒。
sleep()方法不会释放锁,而wait()方法会释放锁。
sleep()方法可以在任意地方使用,并且需要捕获InterruptedException异常,而wait()方法必须在synchronized方法或synchronized块中使用,不需要捕获异常。
https://blog.csdn.net/qq_28958301/article/details/83146528#%E4%B8%80%E3%80%81%E5%9F%BA%E7%A1%80
synchronized既可以加载方法上,也可以加载代码块上,是托管给JVM执行的,而Lock需要显示地指定起始位置和终止位置,是通过代码实现的,它有比synchronized更精确的线程语义。
在资源竞争不是很激烈的情况下,synchronized的性能要优于ReetrantLock,而在资源竞争很激烈的情况下,synchronized的性能会下降的非常快,ReetrantLock的性能基本保持不变。
synchronized获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁,而Lock需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题。
守护线程是指程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。
如果用户线程已经全部退出运行,只剩下守护线程存在了,那么JVM就会退出。
守护线程一般具有较低的优先级。
当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此。
守护线程的一个典型的例子就是垃圾回收器。
在Java中,join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,就是同步执行。
JDBC用于在Java程序中实现数据库操作功能,它提供了执行SQL语句、访问各种数据库的方法,并为各种不同的数据库提供统一的操作接口。
public class JDBCTest {
private static final String USER = "root";
private static final String PASSWORD = "root";
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String DRIVER = "com.mysql.jdbc.Driver";
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName(DRIVER);
connection = DriverManager.getConnection(URL, USER, PASSWORD);
// Statement
statement = connection.createStatement();
statement.execute("DROP TABLE IF EXISTS user");
statement.execute("CREATE TABLE user (id int AUTO_INCREMENT PRIMARY KEY, name varchar(50) NOT NULL)");
statement.execute("INSERT INTO user(name) VALUES (\"YETA\"), (\"RAY\")");
resultSet = statement.executeQuery("SELECT * FROM user");
while (resultSet.next()) {
System.out.println(resultSet.getInt(1) + " " + resultSet.getString(2));
}
// PreparedStatement
preparedStatement = connection.prepareStatement("SELECT * FROM user WHERE id = ?");
preparedStatement.setInt(1, 2);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println(resultSet.getInt(1) + " " + resultSet.getString(2));
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
一个事务是由一条或多条对数据库操作的SQL语句所组成的一个不可分割的工作单元,只有当事务中的所有操作都正常执行完了,整个事务才会被提交给数据库。
commit()方法表示完成对事务的提交,rollback()方法表示完成事务回滚,一般而言,事务默认操作是自动提交。
JDBC的事务隔离级别:
对应MySQL的隔离级别:https://blog.csdn.net/qq_28958301/article/details/89217269#3.1%C2%A0%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB
在Java中,任何类只有被装在到JVM中才能运行。Class.forName()的作用就是把类加载到JVM中,它返回一个与带有给定字符串名的类或接口相关联的Class对象,并且JVM会加载这个类,同时JVM会执行该类的静态代码段。
例如JDBC中,要求Driver类在使用前必须向DriverManager注册自己,当执行Class.forName("com.mysql.jdbc.Driver")时,JVM会加载对应类,并执行静态代码段,在静态代码段中注册到了DriverManager上。
Statement用于执行不带参数的简单SQL语句,并返回它所生成结果的对象,每次执行SQL语句时,数据库都要编译该SQL语句。
PreparedStatement用于执行带参数的预编译SQL语句。
CallableStatement用来调用数据库中的存储过程,如果有输出参数要注册,说明是输出参数。
PreparedStatement比Statement具有以下优点:
JDBC提供了getString()、getInt()和getData()等方法从ResultSet中获取数据,程序会一次性把数据都放到内存中,当数据量大到内存中放不下的时候会抛出异常。
而使用getObject()不会一次性将所有数据都读到内存中,每次调用时才从数据库中去获取数据。