【Java】Java后知后觉(初阶)

那些Java中你可能不知道的一些事<(▰˘◡˘▰)>

IMG_6579.jpg

  1. 关于命令

  • 1
    2
    3
    $ javac HelloWorld.java  
    $ java HelloWorld
    HelloWorld

相信这是大家刚刚接触Java时接触到的第一个程序
在这个命令行终端中我们用到了两个命令javac和java

  • javac后跟着的是一个典型的java文件,javac此时所做的事就是将java源文件编译成字节码文件此时如果编译成功的话是会生成一个名为HelloWorld.class的文件,此时就是成功编译为字节码文件;
  • java后跟的是Java文件中的类名,比如这里的HelloWorld但是不要加.class后缀。
  1. 关于修饰符

-

synchronized修饰符

1
2
3
public synchronized void easonHe(){
...
}

这里的synchronized修饰符是在多线程的程序编码中常会用到的修饰符,synchronized修饰符旨在确保方法在同一时间只能被一个线程访问。同时synchronized可以用于四个访问修饰符。

-

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread implements Runnable{
private volatile boolean active;
public void run(){
active = true;
while(active){
...
}
}
public void stop(){
active = false;
}
}

volatile修饰词也是常用于多线程中,每当线程准备访问volatile所修饰的成员变量时,必须要从共享内存中重新读取该成员变量的值,另外,如果遇到该成员变量的值发生改变时,线程也必须将该成员变量的值写入到共享内存中更新,这样以来,多线程中每个线程所看到的都是成员变量的同一个值。

这个成程序中,若是一个线程调用run()方法,此时另一线程调用stop()方法时,如果线程已经进入while的缓冲区,那么即使stop()方法中的active=false线程也不会停止,但是由于这个线程中的active是被volatile修饰符所修饰,所以这个循环会停止。

-

transient修饰符

1
2
public transient int a = 1;
public int b;

transient修饰符用来定义变量时,其作用是用来预处理类与变量的数据类型,也就是说,如果序列化的对象包含别transient修饰的实例变量,那么JVM将会跳过此特定的变量。

关于运算符

-

位运算符

1
2
3
4
5
6
7
8
9
10
A = 0101 1100
B = 0010 0101
---------------
A&B = 0000 0100
A|B = 0111 1101
A^B = 0111 1001
~A = 1010 0011
B<<2 = 1001 0100
B>>2 = 1001
B>>>2 = 0000 1001
  1. ^如果对应位值相等则为0不等则为1
  2. ~按位取反运算符
  3. *<<按位左移运算符,左操作数按位左移右操作数指定的位数*;
  4. >>按位右移运算符,左操作数按位右移右操作数指定的位数
  5. >>>按位右移补零运算符,在按位右移的情况下将最高位的空位用零填充

但是其实我们真正用到它的时候并非如此使用,而是下面这种情况:

1
2
3
4
5
6
7
8
9
public void Demo(){
public static void main (String[], args){
int a = 13; //即 a=0000 1101
int b = 28; //即 b=0001 1100
int c = 0;
c = a & b;
System.out.println("a & b = "+c) //此时c的值为12 即0000 1100
}
}

也就是说我们平时使用的往往并非二进制编码,而是对应的十进制编码,但有时我们也会为了满足某种需求而使用到十进制编码进行位操作符的运算。

-

短路逻辑运算符

1
2
3
4
5
6
public class Demo{
public static void main(String[], args){
int a = 6;
boolean result = (a<2&&++a<8) //a=6
}
}

这段代码中,result结果为false,因为a<2已经是false了所以结果必定是false,所以说第二个操作判断就不被执行了,也就是++a不被执行,所以此时a仍然为6.

-

instanceof运算符

1
2
3
String str = "watermelon";
boolean isReal = str instanceof String;
// is Real = true

instanceof运算符用于操作一个对象的实例,若为此特定类类型或接口类型则为真,否则为假,即如果运算符左侧所指的对象是右侧类或者接口的一个对象则结果为真。

1
2
3
4
5
6
7
8
9
10
class Cola{
...
}
public class Pepsi extends Cola{
public static void main(String[], args){
Cola cola = new Pepsi();
boolean result = cola instanceof Pepsi;
//此时result=true
}
}

使用instanceof操作符如果被比较的对象兼容于右侧的类型,则同样成立。这里应该注意判断一个实例引用的类型时,使用的是实际类型,而不是声明的类型,如上代码中cola是Pepsi类型而不是Cola类型,并且,子类的实例可以声明为父类,但是父类的实例不可以声明为子类

关于循环

-

加强型for循环(For-Each)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo{
public static void main(String[], args){
int [] num = {2, 6, 9, 30};
for(int i : num){
System.out.print(i+" ");
//此时将会打印出2 6 9 30
}
String [] drinks = {"cola", "coffee", "tea"};
for(String name : drinks){
System.out.print(name+" ");
//此时将会打印出cola coffee tea
}
}
}

这种情况是Java引入的为了用于数组形式的增强版for循环。

ps:在循环或者条件以及选择语句中需要注意的几点:

  1. 在循环操作语句中,如果遇到break语句被执行,那么将会直接跳出最内层的一个循环体,如果遇到continue语句被执行,那么接下来的语句将不会被执行,而是直接进入新一轮的循环,这种用法往往用来选择性退出或者是去刻意忽略某次循环体内的语句;
  2. 在选择操作语句中,如switch语句,如果case没有与变量类型相匹配的,那么将会执行default语句(如果有的话),如果匹配的case语句没有break,那么将会顺序输出以下的语句,直到遇到break或全部输出。

关于Java类

-

包装类

由于java属于面向对象的编程语言,所以我们难免会遇到使用数据时需要使用对象而不是内置数据类型的情形,针对于此,java提供了包装类:Double,Float, Long, Integer, Short, Byte.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class IntergerTest{
public static void main(String[], args){
int a = 128;
Interger b = 128;
Interger c = new Interger(128);
System.out.println(a==b); //true 因为与内置数据类型比较,所以Interger会自动拆箱比较
// a==c 与 a==b情况相同
Interger b1 = 127;
Interger c1 = 127; //此时经编译时为Interger b2 = Interger.valueOf(127)
System.out.println(b1==c1); //true
Interger b2 = 128;
Interger c2 = 128;
System.out.println(b2==c2); //false 因为Interger中的valueOf函数只会对int类型的取值范围内(-128~127)之间的数进行缓存。
Interger b3 = new Interger(127);
Interger c3 = 127;
System.out.println(b3==c3); //false 无论如何Interger与new得到的Interger不会相等,
//因为new得到的对象放在堆存储区里,非new得到的常量则放在常量池即方法区里,故不会经历拆箱,两地址也不同,故不会相等
Interger b4 = new Interger(127);
Interger c4 = new Interger(127);
System.out.println(b4==c4); //false 因为都是通过new得到的对象,所以地址不相同
Interger b5 = 127;
Interger c5 = 127;
System.out.println(b5.equals(c5)); //true
}
}

编译器特别支持的包装成为装箱,所以在内置类型要作为对象使用时,编译器就会将其装箱成为一个包装类,若是一个对象要作为内置数据类型使用时,编译器也会将其拆箱;

要注意在进行比较时 =比较的是地址,equals()比较的是对象的内容,所以在Interger.valueOf()时,范围内相同的值使用同一个地址,但是超出范围后的地址又不相同了。

String类

1
2
3
4
5
6
7
8
public class Demo{
public static void main(){
char[] array = {"E", "a", "s", "o", "n"};
String str = new String(array);
System.out.println(str);
//输出结果为Eason
}
}

在Java中,String字符串属于对象,String类有11中构造方法,这些方法提供不同的参数来初始化字符串,比如这段代码中提供一个字符数组来初始化一个字符串;

1
2
3
4
5
6
7
8
9
10
public class Demo{
public static void main(){
String str = "Eason";
System.out.println(str);
//输出Eason
str = "He";
System.out.println(str);
//输出He
}
}

特别需要注意的是一旦String对象被创建那么它就无法被更改了,也就是说String是被final修饰的,但是这段代码中str在结果上看是改变了,但其实它并没有被更改,因为实例str只是一个String的对象引用,当执行str=”He”的时候它创建了一个新的String对象”He”,原来的”Eason”对象仍然存储在内存中;

也就是说,如果需要对字符串做很多修改,那么应该选择使用StringBuffer和StringBuilder类。
1.StringBuffer:字符串变量,Synchronized线程安全,如果想专成String类型,则可以使用toString()方法,Java.lang.StringBuffer可以通过某些特定的方法调用可以改变该序列的长度和内容,可以将字符串缓冲区安全的应用于多个线程;
2.StringBuilder:字符串变量,非线程安全,在内部StringBuilder对象被当作是一个包含自负序列的变长数组;
总结- 如果操作少量的数据用String - 单线程操作大量数据用StringBuilder - 多线程操作大量数据用StringBuffer。
ps:其实StringBuffer的线程安全也是很低能的也就是说它也只能保证jvm不抛出异常而向下运行而已,所以针对于StringBuilder的高效,所以在绝大部分情况下直接使用StringBuilder

1
2
3
4
String s1 = "a"+"b"+"c";
String s2 = "abc";
System.out.println(s1==s2);//true
System.out.println(s1.equals(s2));//true

这是道面试经常会被问到的问题,在Java常量优化机制中,编译时s1已经成为”abc”在常量池中查找创建,故此时s2就不用再创建了;

1
2
3
4
5
String s1 = "a"+"b";
String s2 = "abc";
String s3 = s1+"c";
System.out.println(s3==s2);//false
System.out.println(s3.equals(s2));//true

这也是道面试经常会被问到的问题,在编译时,”ab”在常量池中被创建其地址为a1,”abc”接着被创建其地址为a2,对于s3,先创建一个StringBuilder或StringBuffer对象,通过append方法连接得到abc,再调用toString()转换为String得到的地址为a3,故==为false,equals比较对象的值为true。

另外:length(), length属性,size()之间的一些区别

  1. length()方法是针对字符串来说的,要求一个字符串的长度时就要用到这个方法;
  2. length属性是针对Java中的数组来说的,要求数组的长度可以用length属性;
  3. size()方法是针对泛型集合List来说的,如果想看一个泛型集合中有多少个元素就使用此方法;

关于数组

-

Arrays类

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.Arrays;
public class Demo{
public static void main(String[], args){
int [] num = new int[8];
int [] num1 = {7,3,6,2,8,5};
Arrays.fill(num, 6);//此时num数组内的元素为66666666
Arrays.fill(num, 1,2,8);//将num数组中第1,2个元素赋值为8
Arrays.sort(num1);//对num1数组进行排序
Arrays.equals(num, num1);//比较两数组元素是否相等
Arrays.binarySearch(num1, 2);//查找元素3在数组中的位置,如果不存在就返回负数
}
}

Java.util.Arrays类能够很方便的处理数组,它提供的所有方法都是静态的,它的部分功能:
1. 通过fill()方法给数组赋值;
2. 通过sort()方法按升序排序;
3. 通过equals()方法判断数组中的元素是否相等;
4. 通过binarySearch()方法能对排好序的数组进行二分法查找等等。

关于正则表达式

-

Pattern与Matcher类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class Demo{
public static void main(String[], args){
String str = "My number is 56? Yes!";
String regex = "(\\d+)(\\D*)(.*)";
Pattern p = Pattern.compile(regex);//创建Pattern对象
Matcher m = p.matcher(str);//创建Matcher(适配器)对象
if(m.find()){
System.out.println(m.group(0));//输出:My number is 56? Yes!
System.out.println(m.group(1));//输出:56
System.out.println(m.group(2));//输出:My number is Yes
System.out.println(m.group(3));//输出:? Yes!
}
}
}

本段代码中group()方法为捕获组,比如正则表达式中((A)(B(C)))这其中就有四个组((A)(B(C)))、(A)、(B(C))、(C),技巧是左边数第几个括号所对应的内容就是相应的第几组;

Pattern与Matcher类都没有公共的构造方法,都是通过调用静态函数所得到的返回类型来创建类;

在其他语言的正则表达式中往往一个反斜杠\就具有转义作用,但是Java的正则表达式中两个反斜杠才能表示转义作用,比如\\d表示一位数字。

在Matcher类中有两个重要的方法就是start()和end()方法,start()方法返回由给定组所捕获的初始索引,end()方法是返回最后一个匹配字符的索引+1

关于Java方法

-

_出现在方法名称

在方法的命名中,下划线可能出现在JUnit测试方法名称中用来分割名称的逻辑组件,典型模式:test 例如:textPop_emptyStack.

命令行参数的使用

1
2
3
4
5
6
7
public class Demo(){
public static void main(String args[]){
for(int i=0;i<args.length;i++){
System.out.println("args["+i+"] = "+args[i]);
}
}
}

运行此程序:

1
2
3
4
5
6
7
$ javac Demo.java
$ java Demo This is a command line
args[0] = This
args[1] = is
args[2] = a
args[3] = command
args[4] = line

这种情况适用于一个程序在运行时再给它传递消息,命令行参数是紧跟在执行程序名称的后边的信息

可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo{
public static void main(){
printMin({2, 1, 5, 0.6,});//输出:The min value is : 0.6
printMin(new double[]{3.2, 4, 9, 6.7})//输出:The min value is : 3.2
}
public void printMin(doulbe... num){
if(num.length==0){
System.out.println("No agument passed");
return;
}
double result = num[0]
for(int i=1;i<num.length;i++){
if(num[i]<result){
result=num[i];
}
}
System.out.println("The min value is : "+result);
}
}

一个函数中最多只能有一个可变参数,并且要放在参数列表的最后,代码在执行时,编译器会将可变参数编译为一个数组,所以在函数的内部,参数名可以看作是数组名;

由于函数重载关系到形参列表,所以当形参列表是多个相同类型的参数这时就与可变参数具有相同的功能,但是对于可变参数的重载,系统有限匹配固定参数的方法

finalize()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FinalizationDemo{
public static void main(String[] args){
Cola cola1 = new Cola(1);
Cola cola2 = new Cola(2);
Cola cola3 = new Cola(3);
cola2 = cola3 = null;//将两对象作废,接下来会被回收
System.gc();//调用Java垃圾收容器
}
}
class Cola extends Object{
private int i;
public Cola(int i){
this.i=i;
System.out.println("Cola object "+i+" is created");
}
protected void finalize() throws java.lang.Throwable{
System.out.println("Cola object "+i+" is disposed");
}
}
//结果:
//Cola object 1 is created
//Cola object 2 is created
//Cola object 3 is created
//Cola object 2 is disposed
//Cola object 3 is disposed

finalize()方法在对象被回收之前调用,它用来清除回收对象,可以用这个方法来确保一个对象打开的文件被关闭,并且该方法需要protected限定其余类不可调用此方法,当然JVM自动完成内存的回收,也可以通过这个方法来手动操作

关于文件的Stream(流)与File(文件)

-

乱码问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo{
public static void main(String[] args){
try{
byte[] bWrite = {1, 3, 5, 7, 9};//写入的数据为byte类型
Outputstream os = new FileOutputStream("test.txt");
for(int i=0;i<bWrite.length;i++){
os.write(bWtite[i]);
}
os.close();

InputStream is = new FileInputStream("test.txt");
int size = is.available();
for(int i=0;i<size;i++){
System.out.println((byte)is.read());//最终要的要将读取数据强制转换成同类型才能避免乱码
}
is.close();
}catch(IOException e){
System.out.println("Exception");
}
}
}

FileOutputStream读写文件中容易出现乱码问题,这跟字符集编码无关,主要是要保证读写的类型一致,当然如果不一致也可以通过OutputStramWriter与InputStreamReader来规定相同的编码

关于scanner类

-

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo{
public static void main(String[] args){
scanner scan = new Scanner(System.in)//控制台输入
//scanner scanf = new Scanner(new File(test.text));从文件中读取

}
if(scan.hasNext()){
String str = scan.next();//单个字符返回,若是空字符则退出
//String str1 = scan.nextLine(); 每次读取一行,包含空字符,这时的判断应为scan.hasNextLine()
//int i = scan.nextInt();如果要接收数据,则为next***() 相应的判断语句也更改
System.out.println(str);
}
}

scanner类在输入时的字符都是可见的,所以带来了一定的安全问题,故可以使用Console类来实现输入密码的目的,即:

1
2
3
Console cns = System.console();
String userame = cns.readLine("User name: ");
char[] password = cns.readPassword("Password: ");

关于Java异常处理

-

异常分类

1、error–错误: 是指程序无法处理的错误,表示应用程序运行时出现的重大错误。例如jvm运行时出现的OutOfMemoryError以及Socket编程时出现的端口占用等程序无法处理的错误。
2、Exception-异常 :异常可分为运行时异常跟编译异常
-运行时异常:即RuntimeException及其之类的异常。这类异常在代码编写的时候不会被编译器所检测出来,是可以不需要被捕获,但是程序员也可以根据需要进行捕获抛出。常见的RUNtimeException有:NullpointException(空指针异常),ClassCastException(类型转换异常),IndexOutOfBoundsException(数组越界异常)等。
-编译异常:RuntimeException以外的异常。这类异常在编译时编译器会提示需要捕获,如果不进行捕获则编译错误。常见编译异常有:IOException(流传输异常),SQLException(数据库操作异常)等。
-

语句块执行问题

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo{
public static void main(String[] args){
try{
//函数语句
}catch(Exception e){
System.out.println("catch is begin");
return 1;//不被执行
}finally{
System.out.println("finally is begin");//被执行
return 0;//被执行
}
}
}

这是一个面试中经常会见到的题目。在这段代码表明了,finally语句块是要在return之前被执行的,并且如果finally语句块中有return语句,则直接执行finally语句块中的return语句,其他语句块中的return就不能被执行了

throw和throws的区别

1
2
3
public void Demo() throws Exception{
throw new Exception();
}

throws经常会被用在方法声明后,方法体之前,表明方法可能跑出一个异常,throw经常被用在方法体内,表示此时抛出一个已定义的异常。

关于继承

-

构造器(构造方法或构造函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father{
father(){
//方法体
}
public father(int i){
//方法体
}
}
public class Son extends father{
Son(){
//super() 自动调用父类的无参构造器
//方法体
}
public Son(int i){
super(3)//主动调用父类的有参构造器,就不再默认调用父类的无参构造器
//方法体
}
}

在Son()继承Father()后,在子类的构造函数中第一行要先调用父类的构造器,可以调用父类的有参构造器(主动调用,如果有的话),也可以不主动调用,但是系统会自动在子类的第一行调用父类的无参构造器,但是如果父类只有一个有参构造器,则在子类的构造器的首行必须要先调用super(参数),否则系统报错

重写(覆盖)中的调用问题

1
2
3
4
5
6
7
8
9
10
class Father{
public void demo(){
System.out.println("Father's demo()");
}
}
public class Son extends Father{
public void demo(){
super.demo();//此时输出Father's demo()
}
}

如果子类对父类的方法进行了重写或覆盖,则若想在子类中调用父类的相同的方法,则需要使用super进行调用

转型问题

1
2
3
4
5
6
7
Father f1 = new Son();//upcasting(向上转型)f1引用指向Son对象
Son s1 = (Son)f1;//downcasting(向下转型)f1引用仍然指向Son对象

----------------------------------------------

Father f2 = new Father();
Son s2 = (Son)f2;//错误,子类引用不能指向父类对象

向上转型:子类对象直接赋给父类引用,不用强制转换
向下转型:把指向子类对象的父类赋给了子类,需要强制转换

关于Java重写(Override)与重载(Overload)

-

向上转型带来的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal{
public void move(){
//Animal方法体
}
}
public class Cat extends Animal{
public void move(){
//Cat重写方法体
}
public void jump(){
//Cat特有方法体
}
public static void main(String[] args){
Animal a = new Animal();
Animal a1 = new Cat();//向上转型
a.move();//调用Animal自己的move()
a1.move();//调用Cat重写过的move()
a1.jump();//error 因为a1的引用类型Animal中没有jump()方法
}
}

这段代码中由于Animal中没有jump()方法导致编译失败,但是可以通过强制转型(UP)来访问引用父类中没有但是子类中有的方法或成员变量

方法重写规则中需要注意的几点:
1. 返回类型可以与被重写方法的返回类型不同,但是得是父类返回值的派生类;
2. 访问权限不能比父类被重写方法的访问权限更低,比如父类方法的访问权限为public,子类重写方法后不能为protected或更低;
4. 重写方法不能抛出更更广泛的强制性异常;

关于Java多态

-

静态方法重写问题

父类的静态方法被子类重写后,调用问题应该看指向子类对象的是父类引用还是子类引用,如果是父类引用指向子类对象则会调用父类的静态方法,如果是子类引用指向子类对象,则会调用子类的静态方法

关于Java接口

-

接口的一些特性

-接口的方法都是且只能是public abstract;
-接口的成员变量都是且只能是public static final;
-Java不支持多继承,但是一个接口可以继承多个接口;
-类继承接口必须要实现接口里的全部方法,除非类为抽象类;
-如果基本功能在不断改变那么要使用抽象类,因为如果要用接口那么功能改变要更改所有继承这个接口的实体内的方法,这也可以理解abstract表示的是”is-a”的一种关系,interface表示的是”has-a”的一种关系
-JDK1.8以后接口中开始允许出现静态方法和方法体;

1
2
3
4
5
6
7
8
9
10
11
public class Demo{
interface demo{
default void test(){
System.out.println("这里是interface里的默认实现方法");
}
static void test1(){
System.out.println("这里是interface里的静态方法");
}
}//接口中只允许这两种非抽象方法实现

}

如果要对接口内的非抽象方法进行调用的话。静态类方法:只能通过接口名调用;default类方法:只能通过接口实现类的类名调用

标记接口

1
2
import java.util;
public interface EventListener{}

这个接口就是监听接口,类似于MouseListener类就继承了这个接口。
标记接口存在的目的:
1.向一个类添加数据类型,因为不需要实现该类的方法(因为本身就没有方法);
2.建立一个公共的父接口,正如EventListener接口,这是由几十个其他接口扩展的Java API,你可以使用一个标记接口来建立一组接口的父接口,它可以使得使用instanceof进行类型查询。例如:当一个接口继承了EventListener接口,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。

如果觉得还不错的话,把它分享给朋友们吧(ง •̀_•́)ง