Java基础部分


一、 安装环境

JVM :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心,具体的下文会详细说明。

JRE :英文名称(Java Runtime Environment),Java运行时环境。它主要包含两个部分,jvm 的标准实现和Java的一些基本类库。它相对于jvm来说,多出来的是一部分的 Java 类库。

JDK :英文名称(Java Development Kit),Java开发工具包。jdk是整个Java开发的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。

显然,这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM。

  • 【问题】Java为什么能跨平台,实现一次编写,多处运行?
    Java 能够跨平台运行的核心在于 JVM 。不是 Java 能够跨平台,而是它的 jvm 能够跨平台。我们知道,不同的操作系统向上的 API 肯定是不同的,那么如果我们想要写一段代码调用系统的声音设备,就需要针对不同系统的 API 写出不同的代码来完成动作。 而 Java 引入了字节码的概念,jvm 只能认识字节码,并将它们解释到系统的 API 调用。针对不同的系统有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有 Windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。引用上面的例子,在 Java API 层面,我们调用系统声音设备的代码是唯一的,和系统无关,编译生成的字节码也是唯一的。但是同一段字节码,在不同的 jvm 实现上会映射到不同系统的 API 调用,从而实现代码的不加修改即可跨平台运行。

参考原文


二、 基本程序设计

1. 学习Java,从【Hello,World!】开始

public class FirstSample{
    public static void main(String[] args){
        System.out.println("Hello,World!");l
    }
}

2. 注释

3. 数据类型

(1) 整型
类型 大小 范围 备注
byte 1字节 -128~127 ( 2^7 ~ 2^7-1 )
short 2字节 -32768~32767 ( 2^15 ~ 2^15-1 )
int 4字节 -2147483648~2147483647 ( 2^15 ~ 2^15-1)刚好超过20亿
long 8字节 -9223372036854775808~9223372036854775807 (2^31 ~ 2^31-1)

【注】

  • long数值有一个后缀L或l
  • 把一个较大的值赋给int变量,系统是不会自动转成long的,除非再后面加L
  • Java没有任何形式的无符号类型(undigned)
(2) 浮点类型
类型 大小 范围 备注
float 4字节 大约±3.40282347E+38F (有效位数为6~7位)
short 8字节 大约±1.79769313486231570E+38F (有效位数15位)

【注】

  • float数值有一个后缀F或f
  • 下面是用于表示溢出和出错的三个特殊浮点数值
    • 正无穷大 ——> Double.POSITIVE_INFINITY(=1.0/0.0)
    • 负无穷大 ——> Double.NEGATIVE_INFINITY(-1.0/0.0)
    • NaN(非数字) ——> Double.NaN(0.0d/0.0)
  • 浮点数值不适合用于无法接受舍入误差的金融计算中。浮点数采用二进制数表示(二进制无法精确表示1/10,就像十进制无法精确表示1/3),System.out.println(2.0-1.1)将打印出0.89999999999999,而不是0.9。应该使用BigDecimal类。
//由于精度原因,不能用等号判断两个小数是否相等

double double d1,d2;
 ···
   if(d1==d2)
 ···
(3) char类型

关于字符和字节的问题

  • ASCII码:一个ASKII码是一个字节
  • UTF-8编码:一个英文字符等于一个字节,一个中文字符等于三个字节
  • Unicode编码:一个英文等于两个字节,一个中文(含繁体)等于两个字节

char类型的变量经过 + 、-等操作后,得到的值为int类型。所以,将char类型的变量转化为int类型,可以使用c-'0'的方法。

    char c1 = '1';
    char c2 = '2';
    //char c3 = c1+c2;
    int i1 = c1 + c2;
    int i2 = c1 - '0';
(4) boolean类型
(5) 类型间的相互转换

Java支持自动类型转换的类型:

  • byte——>short——>int——>long——>float——>double
  • char——>int——>long——>float——>double

类型提升规则:

  1. 所有的byte,short,char都将被提升到int
  2. 整个算数表达式的数据类型,自动提升到与表达式中最高等级操作数同样的类型。
//运行下面的程序,正确的结果是:
class func{
    public static void main(String[] args){
        byte a = 1;
        System.out.println(a += 1); // a+=1等价于a=(int)(a+1),+=运算符自动转换,但a=a+1就会报错了
        byte f = a + 1;
        System.out.println(f);
    }
}

答案:2,错误

4.字符串

(1) 不可变字符串
String greeting = "Hello";
greeting = greeting.substring(0,3) + "p!";
greeting = "Help!"

由于不能修改Java字符串中的字符,所以在Java文档中将String类对象称为不可变字符串。你可能会问,上面的代码不是将greeting由"Hello"修改为"Help!"了吗?不是的,greeting只是一个引用,所以说,这里的字符串并不是可变,只是变更了字符串引用。

(2)检测字符串是否相等

可以使用equals方法检测两个字符串是否相等。

s.equals(t);
"Hello".equals(greeting); //推荐,常量放在前面可以避免空指针异常

一定不要用==检测两个字符串是否相等。==只能确定两个字符串是否放在同一个位置上(即判断地址是否相同)。JVM只会将字符串常量共享,而"new"/"substring"/"+"等操作产生的结果并不是共享的。

public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2); //true
        String s3 = new String("abc");
        System.out.println(s1 == s3); //false
    }
}
//空串判断方式
if(str.length()==0)
//或者
if(str.equals(""))

String类型变量还可以存放一个特殊的值null。

if(str == null) //检查str是否为null
if(str != null && str.length() != 0) //既不是null也不是空串

有时候需要由较短的字符串构建字符串。采用字符串拼接的方式达到此目的效率较低,每次拼接字符串,都会构建一个新的string对象,造成了对时间和空间的浪费。使用StringBuilder可以避免这个问题。

StringBuilder builder = new StringBuilder;
builder.append(ch);
builder.append(str);
String completedString = builder.toString();

StringBuilder这个类的前身是StringBuffer,其效率有些低,但允许采用多线程的方式进行添加和删除字符的操作(StringBuffer是线程安全的)。单线程的情况下则应该使用StringBuilder.这两个类的API是相同的。

5.输入输出

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextInt()) {
            //注意while处理多个case              
            int a = in.nextInt();
            int b = in.nextInt();
            System.out.println(a + b);
        }
    }
}

常用API


三、面向对象

面向对象的三大特征:封装、继承、多态。

1. 类与对象

类Class是对象的特征抽象,对象是类的具体实现。

2. 封装

3. 继承

子类继承父类,Java中使用extends关键字。

class SuperClass{
    private int a = 5;
    public int getA(){
        return a;
    }
}

public class SubClass1 extends SuperClass{
    //继承的类不能直接访问父类的私有域,只能调用公有的get方法
    public int getA(){
        return a+1; //won't work
    }

    //必须加上spuer.getA(),否则会自己调用自己,直到程序崩溃
    public int getA(){
        return getA()+1; //still won't work
    }
}

public class SubClass2 extends SuperClass{
    private int a = 7;
    public int getA(){
        return a+1;
    }
    public int getSuperAAddOne(){
        return super.getA()+1;
    }

    public static void main(String[] args) {
        SubClass2 subClass = new SubClass();
        System.out.println(subClass.getA()); //8
        System.out.println(subClass.getSuperAAddOne());//6
    }
}

覆盖和重载没什么关系。重载是在一个类里,根据参数的个数及类型的不同,对方法进行重载。两者没有任何联系。

4. 多态

在Java中,对象变量是多态的,一个SuperClass变量既可以引用一个SuperClass对象,也可以引用任何一个子类的对象(如SubClass1,SubClass2等)。

//父类引用可以指向子类对象
SuperClass superClass1 = new SuperClass();
SuperClass subClass1 = new SubClass();

//然而,子类引用不能指向父类对象
SubClass subClass2 = superClass1;//error

//子类数组的引用可以直接转换成父类数组的引用,不需要强制转换
SubClass subClass3 = new SuperClass();
SuperClass superClass2[] = subClass3;

上面代码中,subClass1可以调用可以调用父类的方法,但反过来不行。

调用过程:

第一步:编译器查看对象的声明类型和方法名,假设要调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是,有可能存在多个名字为f,但参数类型不一样的方法。例如,可能会存在方法f(int)和f(String)。编译器将会一一列举出C类中名为f的方法 和其父类中访问属性为public且名为f的方法(父类的私有方法不可以访问)。 至此,编译器已经获得所有可能被调用的候选方法。

第二步:编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的类型完全匹配,就选这个方法,这个过程叫做重载解析。例如,对于x.f("Hello")来说,编译器会挑选f(String),而不是f(int).由于允许类型转换(int 可以转成double),所一这个过程可能很复杂,如果编译器没有找到与参数类型匹配的方法,或者经过类型转换后有多个类型与之匹配,就会报错。 至此,编译器已获得需要调用的方法名字和参数类型。

[注释]: 静态绑定:如果是private方法、static方法、final方法、构造方法,那么编译器可以准确的知道该调用哪个方法,这种调用成为静态绑定。 动态绑定:调用的方法依赖于隐式参数(隐式参数就是调用对象)的实际类型,在运行时实现动态绑定。

当程序运行,并且采用动态绑定调用方法时,虚拟机通过搜索,调用与x所引用的对象实际类型最合适的那个类的方法。先在自己的类中找,如果找不到,再去其父类中寻找。 每次调用方法都搜索,时间开销太大。因此,虚拟机预先为 每个类 创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。

动态绑定有一个非常重要的特性:无需对现存的代码进行更改,就可以对程序进行拓展。

5. final关键字:阻止继承

final关键字的几种用法:

6. 抽象类

7. Object类:所有类的父类

Object类中方法概览:

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {}

Java中所有类的始祖,在Java中每个类都是由它拓展而来的。

//String类使用下列算法计算散列码
int hash = 0;
for(int i=0;i<length;i++)
    hash = 31*hash + charAt(i);
class StringTest{
    public static void main(String[] args) {
        int[] num = {1,2,3,4,5}; 
        System.out.println(num); // [I@1b6d3586 前缀I表示是整型数组
        System.out.println(Arrays.toString(num));// [1, 2, 3, 4, 5] 
    }
}

8. 参数数量可变的方法

可以通过...定义变参方法。

public static double max(double... values){
        double maxValue = Double.NEGATIVE_INFINITY;
        for(double v : values)
            if(v>maxValue)
                maxValue = v;
        return maxValue;
    }

9. 枚举类 Enum

Java5中新增了一个enum关键字,用以定义枚举类。

//SeasonEnum.java
public enum SeasonEnum {
    SPRING("春天"),SUMMER("夏天"),FALL("秋天"),WINTER("冬天");
    private String desc;
    private SeasonEnum(String desc){
        this.desc = desc;
    }
    public String getDesc(){
        return desc;
    }
}

//EnumTest.java
public class EnumTest {
    public static void main(String[] args) {
        //所有的枚举类都有一个values()方法,返回该枚举类值的数组
        //SeasonEnum[] se = SeasonEnum.values();
        for(SeasonEnum s:SeasonEnum.values()){
            System.out.println(s);
        }
        SeasonEnum se = Enum.valueOf(SeasonEnum.class, "SPRING"); //
        System.out.println(se.getDesc()); //春天
        System.out.println(se.ordinal()); //0
    }
}

所有的枚举类型都是Enum类的子类,他们继承了Enum类的许多方法

【API】java.lang.Enum<E>

10. 反射

11. 接口

接口不是类,而是对类的一组描述,这些类要遵从接口描述的统一格式进行定义。一个类可以实现(implement)一个或多个接口。 下面是Comparable接口的代码,这段代码表明。任何实现Comparale接口的类都要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值。

public interface Comparable<T>{
    //接口中的方法自动地属于public,因此不必提供任何关键字
    int compareTo(Object other); 
}
接口的特性:
接口的使用:
/**
 *  工具函数: 对JSONArray,按照allCount从大到小进行排序
 * @param jsonArray
 * @return
 */
public JSONArray sortJSONArray(JSONArray jsonArray){
    List<JSONObject> list = JSONArray.parseArray(jsonArray.toJSONString(), JSONObject.class);
    //普通匿名内部类法,它的含义是,创建一个实现Comparator接口的类的新对象
    Collections.sort(list, new Comparator<JSONObject>() {
        @Override
        public int compare(JSONObject o1, JSONObject o2) {
            int a = o1.getInteger("allCount");
            int b = o2.getInteger("allCount");
            if (a > b) {
                return -1;
            } else if(a == b) {
                return 0;
            } else
                return 1;
            }
    });

    //lambda表达式写法
    Collections.sort(list, (JSONObject o1, JSONObject o2)->{
            int a = o1.getInteger("allCount");
            int b = o2.getInteger("allCount");
            if (a > b)
                return -1;
            else if(a == b)
                return 0;
            else
                return 1;
    });

    JSONArray jsonSort = JSONArray.parseArray(list.toString());
    return jsonSort;
}

12. lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

(String first,String second)->first.length-second.length

(String first,String second)->{
    if(first.length < second.length) return -1;
    else if(first.length > second.length) return 1;
    else return 0;
}

//无参也要保留括号
()->{for(int i=0;i<10;i++) System.out.println("i");}
package package6;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class LambdaTest {
    public static void main(String[] args){
        List<String> languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

        System.out.println("Languages which starts with J :");
        filter(languages, (str)->str.startsWith("J")); //Java

        System.out.println("Languages which ends with a ");
        filter(languages, (str)->str.endsWith("a")); //Java Scala

        System.out.println("Print all languages :");
        filter(languages, (str)->true); //Java Scala C++ Haskell Lisp

        System.out.println("Print no language : ");
        filter(languages, (str)->false);

        System.out.println("Print language whose length greater than 4:");
        filter(languages, (str)->str.length() > 4); //Scala Haskell
    }

    public static void filter(List<String> names, Predicate<String> condition) {
        for(String name: names)  {
            if(condition.test(name)) {
                System.out.println(name + " ");
            }
        }
    }
}
  

在java 8中已经为我们定义了很多常用的函数式接口它们都放在java.util.function包下面,一般有以下常用的四大核心接口:

函数式接口 类型 参数类型 返回类型 抽象方法名 描述
Consumer<T> 消费型接口 T void void accept(T t) 对类型为T的对象应用操作
Supplier<T> 供给型接口 T T get() 返回类型为T的对象
Function<T, R> 函数型接口 T R R apply(T t) 对类型为T的对象应用操作并返回R类型的对象
Predicate<T> 断言型接口 T boolean boolean test(T t) 确定类型为T的对象是否满足约束
package package6;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class LambdaTest2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(4,7,9);
        // Lambda表达式
        list.forEach(a -> System.out.println(a));
        System.out.println("********************");
        // 方法引用
        list.forEach(System.out::println);
    }
}

表达式System.out::println就是一个方法引用(method reference),它等价于lambda表达式x->System.out.println(x)

package package6;

import java.util.Arrays;
import java.util.List;

public class LambdaTest2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(4,7,9);
        int printCount = 0;
        //下面这种写法是错误的
        list.forEach(a -> {
            System.out.println(a);
            //下面这句会产生错误
            printCount++;
        });
        //可以使用for循环
        for(Integer a: list){
            System.out.println(a);
            printCount++;
        }
    }
}

13. 内部类

14. 代理


4. 异常

1. 异常

(1) 异常分类

Java异常被分为两大类,Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常,其余的异常被称为Checked异常。 在Java中,异常对象都是派生于Throwable类的一个实例。所有的异常都是由Throwable继承而来,但在下一层立即分为了两个分支:Error和Exception Java中异常层次结构 Error通常是系统内部错误或者资源耗尽错误,通常会告诉用户错误并安全终止程序; Exception又分为两个分支,一类派生于RuntimeException,另一个派生于其他异常。

原则:“如果出现Runtime异常,就是你的问题”。

(2) 异常处理
try...catch捕获异常
try{
    //TO DO... 
    // 出现异常,生成异常对象ex
}catch(ExceptionClass1 e1){// ex instanceof ExceptionClass1 == true
    //exception handler statement...
}catch(ExceptionClass2 e2){// ex instanceof ExceptionClass2 == true
    //exception handler statement...
}catch(ExceptionClass3|ExceptionClass4 e3){// (ex instanceof ExceptionClass3 == true)||(ex instanceof ExceptionClass4 == true)
    //exception handler statement...
}
... ...

如果执行try块代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境(JRE),这个过程被称为抛出(throw)异常。 当JRE收到异常对象时,会寻找能处理异常对象的catch块,如果找到了,就交给对应的catch块处理,这个过程称为捕获(catch)异常。

使用finally回收资源

有时候,程序在try块中打开了资源(如数据库连接),这些资源需要被回收。 不管是否有异常被捕获,finally子句中代码始终被执行。

try{
    1
    可能抛异常的语句
    2
}catch(Exception e){
    3
    可能抛异常的语句
    4
}finally{
    5
}
    6

由此可见,finally语句始终被执行。

注意:假设利用return语句从try块中退出,在try块中的return执行之前,finally语句将会被执行,如果finally语句中也有返回值,将覆盖原始的返回值

public class FinallyTest {
    public static int f(int n){
        try{
            int r = n*n;
            return n;
        }finally{
            if(n == 2)
                return 0;
        }
    }

    public static void main(String[] args) {
        System.out.println(f(2));//0
    }
}

访问异常信息

所有的异常对象都包含如下方法:

2. 断言


5. 泛型

为什么要有泛型:类型参数可以使程序具有更好的可读性和安全性

1.泛型类

public class Pair<T>{
    private T first;
    public Pair(T first){ this.first = first;}
    public getFirst(){ return first; }
    public setFirst(T newValue){ first = newValue; }
}

//泛型类可以引入多个变量
public class Pair<T,U>{ ... }

2.泛型方法

class ArrqyAlg{
    public static <T> T getMiddle(T... a){
        return a[a.length / 2];
    }
}

//调用
String middle = ArrAlg.<String>getMiddle("John","Q.","Public");
//也可以省略<>,编译器可以自己推断出来
String middle = ArrAlg.getMiddle("John","Q.","Public");

有时,需要对泛型变量加以约束

//这里要注意,只是增加条件对变量进行限定,而不是替代变量,和通配符不一样
public static <T extends Comparable> T min(T[] a){ ... }

3.泛型代码与虚拟机

虚拟机没有泛型对象,所有对象都属于普通类。

(1) 类型擦除
(2) 不能用基本类型实例化类型参数

没有Pair<double>,只有Pair<Double>

(3) 类型查询

在虚拟机中,对象总有一个特定非泛型类型,即原始类型,所以当类型查询时,只产生原始类型。

Pair<String> stringPair = ...;
Pair<Double> doublePair = ...;
if(stringPair.getClass == doublePair.getClass) //总是true,都是Pair.class

if(doublePair instanceof Pair<String>) //实际上只是检测是不是任意类型的Pair
if(doublePair instanceof Pair<T>) //实际上只是检测是不是任意类型的Pair
(4) 不能创建参数化类型的数组
List<String>[] list = (List<String>[]) new ArrayList<?>[10];
(5) 泛型类型的继承规则

虽然ManagerEmployee的子类,但Pair<Manager>Pair<Employee>却没有任何关系。

4.通配符类型

Pair<? extends Employee> //子类型限定通配符
Pair<? super Employee> //父类型限定通配符
Pair<?> //无限定通配符 个人认为相当于Pair<? extends Object>