返回
Featured image of post java

java

后端编程原神!启动!

JAVA

启程

java从源代码到运行需要经过两个步骤:

  • 通过javac将.java源文件编译为.class字节码文件
  • 通过java运行字节码文件,该步骤可以分为两种处理方式:
    • 字节码由JVM逐条解释
    • 部分字节码由JIT直接编译为机器指令后执行

所以理论上来说java既是编译型也是解释型,但是JIT是后出的,所以一般还是认为java是编译型语言

数据类型

java的数据类型跟C++几乎没有什么差别,这里直接开始说明java的包装器类型:

Byte-> byte Short-> short Integer-> int Long-> long Float-> float Double-> double Character-> char Boolean-> boolean

包装器类型可以理解成基础数据类型做了一层包装,其具有更加适用的方法,同时为了很好的与基础数据结构兼容,java5实现了自动装箱和自动拆箱机制来使其直接进行类型的兼容:


Integer integerValue = new Integer(42);


String numberString = "123";
int parsedNumber = Integer.parseInt(numberString);

Character charValue = new Character('A');

char testChar = '9';
if (Character.isDigit(testChar)) {
System.out.println("");
}
Integer integerValue = 42; //自动装箱,等同于int xxx =  42
int primitiveValue = integerValue; //自动拆箱,等同于integerValue.intValue()

引用数据类型

引用在C++和函数的相关地方已经在之前有一定的了解了。String是java最典型的引用数据类型。相较于常规的数据类型,引用数据类型的初始化时null(包括数组和接口)。

对于基本的数据类型:

  • 1、变量名指向具体的数值
  • 2、基本数据类型存储在栈上

对于引用数据类型:

  • 1、变量名指向存储对象的内存地址,在栈上
  • 2、内存地址指向的对象存储在堆上

堆是程序运行时申请的空间,使用new时就是在申请堆空间。栈能够和处理器直接关联,速度快,但是空间小。堆速度略慢,但是空间较大,可以方便的申请。

数据类型转换和缓存池

数据类型转换也是老生常谈的问题了:

byte -> short -> int -> long -> float -> double
char -> int -> long -> float -> double

除了自动的无损类型转换,还可以使用强制类型转换,转换方式类似C/C++

a = (int)b;

java的基础数据类型其实有一点类似python的性质,常用的数据类型会被存在缓存池里:

Integer x = new Integer(18);
Integer y = new Integer(18);
System.out.println(x == y);

Integer z = Integer.valueOf(18);
Integer k = Integer.valueOf(18);
System.out.println(z == k);

Integer m = Integer.valueOf(300);
Integer p = Integer.valueOf(300);
System.out.println(m == p);

可以参考一下valueOf的实现:

public static Integer valueOf(int i) {
    if (i >=IntegerCache.low && i <=IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

其中各种包装数据类型各有一定值在缓冲池中:

  • byte, short, int, long 都是-128~127
  • char 是 \u0000~\u007f
  • boolean 是true、false

由于Integer.valueOf算是包装和优化了new,所以在某个角度上来看可以多使用valueOf而不是简单的new。

流程控制

java的流程控制和C/C++几乎完全一样,但是有一个比较特殊的for-each可以直接调用数组:

String []strs = ["a", "b"];
for(String str:strs){
    System.out.println(str);
}

数组

数组是接触每个语言非常常用的东西,java的数组也是比较混沌的存在。

数组的定义和初始化同样是类似C/C++的方式:

int[] anArray = new int[10];

虽然数组方便定义,但是实际上我们不经常使用数组进行各种操作(因为没有常用的方法),所以可以转化成ArrayList类型:

int[] anArray = new int[] {1, 2, 3, 4, 5};

List<Integer> aList = new ArrayList<>();
for (int element : anArray) {
    aList.add(element);
}

上述代码以一种比较复杂的方式实现了从数组到对应包装类型的转换,实际上也有一些方便的方法:

List<Integer> aList = Arrays.asList(anArray);


List<Integer> aList = Arrays.stream(anArray).boxed().collect(Collectors.toList());

上面第一行代码实际上并不能成功运行,因为asList中要求的参数是Integer数组,而anArray是int类型,所以下面的代码才是正确的转化方式(真麻烦)。

另一个比较常用的操作是sort(),同样是ArrayList才有的操作:

int[] anArray = new int[] {5, 2, 1, 4, 8};
Arrays.sort(anArray);

该函数对数组是原地排序的,所以不需要接返回值。至于反序等复杂操作:

Arrays.sort(yetAnotherArray, 1, 3,
                Comparator.comparing(String::toString).reversed());

查找也可以使用高效的方法实现(前提是经过排序):

int index = Arrays.binarySearch(anArray, 4);

多维数组仿照C++理解即可。

关于数组的输出,在前面知道了数组的toString实现不包含其中元素的输出,而是仅仅输出数组的属性和hashcode等,因此比较拉跨。

数组的输出方式有多种,主要可以分为基于stream,基于for循环以及基于Arrays。首先是基于stream的,现在看起来还是相当复杂的东西:

Arrays.asList(cmowers).stream().forEach(s -> System.out.println(s));
Stream.of(cmowers).forEach(System.out::println);
Arrays.stream(cmowers).forEach(System.out::println);

其次是基于for和for-each的

for(int i = 0; i < cmowers.length; i++){
    System.out.println(cmowers[i]);
}

for (String s : cmowers) {
    System.out.println(s);
}

还有基于Array方法类的,这个应该是最方便实用的:

String [] cmowers = {"沉默","王二","一枚有趣的程序员"};
System.out.println(Arrays.toString(cmowers));

String[][] deepArray = new String[][] {{"沉默", "王二"}, {"一枚有趣的程序员"}};
System.out.println(Arrays.deepToString(deepArray));

String类

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
}

从这里可以看到String类的声明,final说明其不能被继承,Serializable说明其可以序列化,Comparable说明其可以使用compareTo()方法进行比较(而不是==) ,最后CharSequenceString、StringBuffer都实现了,但是由于String不可变,所以后续也会说道StringBuffer等。

java类底层实现基于char[](在java9之后变成了byte[]),使用byte[]可以节省内存(在大多数java程序中字符串内存占用最大),但是需要做编码检测,现在的版本就是如果存在二字节字符就全使用二字节(UTF-16),否则使用一字节存储(Latin)。

但是为什么不使用UTF-8而是UTF-16?因为UTF-8使用变长编码(从1到4字节),对于java字符串做随机访问会非常麻烦。虽然UTF-16也是变长(2、4字节),但是由于Java随机访问的方法一般是以char为单位的(有可能被从中间截断)

String类实现了一些常用的方法:包括hashCode、substring、indexOf等。hashCode使用了31倍hash法,同时进行了缓存优化,记录计算得到的hash值到hash变量中:

private int hash; // Cache the hash code for the string

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

subString的实现倒是不复杂,但是可以参考一下如何应用:

String str = "   Hello,   world!  ";
String trimmed = str.trim();                  // 去除字符串开头和结尾的空格
String[] words = trimmed.split("\\s+");       // 将字符串按照空格分隔成单词数组
String firstWord = words[0].substring(0, 1);  // 提取第一个单词的首字母
System.out.println(firstWord);                // 输出 "H"

查找第一次出现的字符可以使用indexOf,应用如下:

String str = "Hello, world!";
int index = str.indexOf("world");  // 查找 "world" 子字符串在 str 中第一次出现的位置
System.out.println(index);        // 输出 7

String str = "Hello, world!";
int index1 = str.indexOf("o");    // 查找 "o" 子字符串在 str 中第一次出现的位置
int index2 = str.indexOf("o", 5); // 从索引为5的位置开始查找 "o" 子字符串在 str 中第一次出现的位置
System.out.println(index1);       // 输出 4
System.out.println(index2);       // 输出 8

除此之外还有length(),isEmpty(),charAt(),trim()等等

String不可变原因有三:1、安全2、hash保存不经常变3、字符串常量池可以节省空间。因为其不变,所以String类的方法通常都是返回一个新的String对象。

当执行String s = new String(" ");操作时,会创建两个对象,在进行new操作时会首先在字符串常量池里查找是否有" "这个对象,如果没有的话就会在字符串常量池里创建一个,在堆中创建一个,并把堆中的地址返回给s。但是要注意的是,使用new始终会创建一个对象不管字符串的内容是否存在,而使用双引号会重复利用字符串常量中的对象:

String s = new String("二哥");
String s1 = new String("二哥");

String s = "三妹";
String s1 = "三妹";

前两行代码会创建三个对象,常量池1个,堆上2个。而下面两行只会创建一个位于常量池的对象。

image-20231014210441067
image-20231014210441067

为了充分利用常量池,字符串还有个intern方法:

String s1 = new String("二哥三妹");
String s2 = s1.intern();
System.out.println(s1 == s2);
#false
    
String s1 = new String("二哥") + new String("三妹"); 
# new StringBuilder().append("二哥").append("三妹").toString();
String s2 = s1.intern();
System.out.println(s1 == s2);
# true

根据结果知道两者的地址不同,s2使用的是字符串常量池中的所以不同。第二段代码则是由于s1引用了堆中“二哥三妹”对象,但是字符串常量池里不存在,所以s2使用的是堆中的对象。

由于String是不可变的,为了考虑性能,java引入了stringBuilder,StringBuffer类,两者实现差不多,不同的是StringBuffer使用了synchronized关键字来保证线程安全,但是StringBuilder性能更快,在单线程下使用(理论上多线程也可以想办法避免冲突)

前面提到了String类型的比较,实际上比较方式有两种,比较内容用.equals比较地址用==

String的拼接方法有很多中,常用的主要是+appendconcatjoin等。其中+调用的是StringBuilder的append方法,性能很高,concat在连接空字符串时性能会较慢。

String的分割其实和python比较类似,但是由于存在特殊符号的问题导致实际使用起来并不方便(详细参考:https://javabetter.cn/string/split.html):

面向对象

从某个角度来看几乎和C++一模一样:

public class Person {
    private String name;
    private int age;
    private int sex;

    public Person(String name, int age, int sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public static void main(String[] args) {
        Person person = new Person("沉默王二", 18, 1);

        System.out.println(person.name);
        System.out.println(person.age);
        System.out.println(person.sex);
    }
}

java的包使用package关键字,位于一个包类可以访问包作用域的字段和方法(不使用作用域关键字进行修饰的方法就是包的作用域):

java的导包使用import关键字进行。

在构建类时,对象包括临时变量、成员变量、静态变量(类变量)、常量(使用final修饰)

java的方法构造:

image-20231014221039589
image-20231014221039589

  • public:该方法可以被所有类访问。
  • private:该方法只能在定义它的类中访问。
  • protected:该方法可以被同一个包中的类,或者不同包中的子类访问。
  • default:如果一个方法没有使用任何访问权限修饰符,那么它是 package-private 的,意味着该方法只能被同一个包中的类可见。

方法分为预定义好的方法(类似python的魔法方法),以及自定义的方法。

在定义方法时,分为三种:

  • 实例方法(对象方法)
  • 静态方法(类方法)
  • 抽象方法(当有抽象方法时,该类也必须是抽象类)

java的参数在1.5之后引入了可变参数,必须放在参数列表的最后但是同时也不提倡使用该东西(类似python的**kwargs和*args)而且只能存在一个

而且在重载带有可变参数的方法时必须明确指示:

public static void main(String[] args) {
    String [] strs = null;
    print(strs);

    Integer [] ints = null;
    print(ints);
}
public static void main(String[] args) {  //wrong use
    print(null);
}


public static void print(String... strs) {
}

public static void print(Integer... ints) {
}

JNI (java native interface)可以调用C++/C等代码,但是目前应该用不到

之前是java的类方法,了解完类方法之后再进行构造函数的学习就好很多了。java的类构造其实和C++也高度类似。其可以定义也可以缺省。构造方法必须满足如下规则:

  • 构造方法的名字必须和类名一样;
  • 构造方法没有返回类型,包括 void;
  • 构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。

默认构造方式是会为属性分配默认值的。有参构造需要手动写参数构造。此外也可以使用重载方式进行构造方法的重载。

除了构造方法,往往还会用到复制方法。复制的话可以通过三种方式进行:

  • 通过构造方法(需要自己写)
  • 通过对象的值(依次赋值)
  • 通过 Object 类的 clone() 方法

在类定义的时候,如何需要实现clone方法,需要使用Cloneable接口,没有实现该接口时无法使用。浅拷贝和深拷贝的区别已经很清晰了,但是使用clone方法进行深拷贝的时候需要对所有的引用类型都实现clone方法,会显得相当笨重,这时候就可以使用序列化方法

Licensed under CC BY-NC-SA 4.0