final关键字

  • final 变量定义:
    • 自定义常量: 如PI,
      • final 数据类型 变量名 =值
      • 常量的变量名大写 ,多个单词用_连接

概述

  • A:为什么会有final
    • 由于继承中有一个方法重写的现象,而有时候我们不想让子类去重写父类的方法.这对这种情况java就给我们提供了一个关键字: final
  • B:final概述
    • final关键字是最终的意思,可以修饰类,变量,成员方法。
  • C:final修饰特点
    • 修饰类: 被修饰类不能被继承
    • 修饰方法: 被修饰的方法不能被重写
    • 修饰变量: 被修饰的变量不能被重新赋值,因为这个量其实是一个常量
  • **D:final关键字修饰局部变量 **
    • 基本类型,是值不能被改变
    • 引用类型,是地址值不能被改变

final,finally,finalize有什么不同?

  • final可以修饰类,方法,变量
    • final修饰类代表类不可以继承拓展
    • final修饰变量表示变量不可以修改
    • final修饰方法表示方法不可以被重写
  • finally则是Java保证重点代码一定要被执行的一种机制
    • 可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC连接、保证 unlock 锁等动作。
    • 在以下4种特殊情况下,finally块不会被执行: 1. 在finally语句块中发生了异常。2. 在前面的代码中用了System.exit()退出程序。3. 程序所在的线程死亡。4. 关闭CPU。
  • finalize 是基础类 java.lang.Object的一个方法
    • 它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9开始被标记为 deprecated。

深入理解

  • 可以将方法或者类声明为 final,这样就可以明确告知别人,这些行为是不许修改的。
  • 如果你关注过 Java 核心类库的定义或源码, 有没有发现java.lang 包下面的很多类,相当一部分都被声明成为final class?在第三方类库的一些基础类中同样如此,这可以有效避免 API 使用者更改基础功能,某种程度上,这是保证平台安全的必要手段。
  • 使用 final 修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成 final。
  • final 变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值 final 变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。

Objecg类

  • java.lang包下,是所有类的父类

公用方法

方法名 描述
System.gc(); 强制垃圾回收(只是通知)
finalize(); 回收之前调用的方法,需要在对象的类中重写,该方法中的this表示回收的垃圾对象
equals() 比较对象地址值是否相等
clone() 克隆一个新的对象
toString() 返回对象的字符串形式
getClass() 返回和当前对象相关的Class对象.getName 获取对象的类型
hashCode() 获取对象的哈希值,用于equeal比较,建议重写
notify,notifyall,wait 用来对给定对象进行线程同步的

Java 拷贝

  • java的赋值都是传值的,对于基础类型来说,会拷贝具体的内容,但是对于引用对象来说,存储的这个值只是指向实际对象的地址,拷贝也只会拷贝引用地址。

  • java中所有的对象都是继承自java.lang.Object。Object对象中提供了一个clone方法,来供我们对java对象进行拷贝。protected native Object clone() throws CloneNotSupportedException;

    • 方法是native的,所以不需要我们来实现

    • clone方法还是protected,这意味着clone方法只能在java.lang包或者其子类可见。

    • 如果我们想要在一个程序中调用某个对象的clone方法则是不可以的。因为clone方法是定义在Object中的,该对象并没有对外可见的clone方法。

    • JDK的建议是让我们去实现接口Cloneable,实现了这个接口就表示这个对象可以调用Object的clone方法。

      1
      2
      3
      // Cloneable是空的,明没有强制要你去实现clone方法。
      public interface Cloneable {
      }
    • clone只是对象的拷贝,它只是简单的拷贝对象,而不会去执行对象的构造函数。clone会导致浅拷贝的问题。

  • 根据对对象属性的拷贝程度(基本数据类和引用类型),会分为两种:

    • 浅拷贝 (Shallow Copy)
    • 深拷贝 (Deep Copy)

浅拷贝

  • 只是增加了一个指针指向已存在的内存地址
    • 基本类型,拷贝的就是基本类型的值;
    • 如果属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
    • 即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

浅拷贝的实现

  • 实现对象拷贝的类,需要实现 Cloneable 接口,并覆写 clone() 方法。

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    class Student implements Cloneable {
    //引用类型
    private Teacher teacher;
    //基础数据类型
    private String name;


    public Teacher getTeacher() {
    return teacher;
    }

    public void setTeacher(Teacher teacher) {
    this.teacher = teacher;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    protected Object clone() {
    try {
    // 浅拷贝:直接调用父类的clone()方法
    return super.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    return null;
    }
    }

    @Override
    public String toString() {
    return "[Student: " + this.hashCode()
    + ",Teacher:" + this.getTeacher()+":"
    + this.getTeacher().getName()
    + ",name:" + this.getName() + "]";
    }
    }

    class Teacher {
    private String name;

    public String getName() {
    return name;
    }

    public Teacher(String name) {
    this.name = name;
    }
    }
  • 测试clone结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) {
    Student studentA = new Student();
    studentA.setName("张三");
    studentA.setTeacher(new Teacher("老师1"));
    //克隆
    Student studentB = (Student) studentA.clone();
    studentB.setName("李四");
    System.out.println("studentA:" + studentA.toString());
    System.out.println("studentB:" + studentB.toString());
    // 修改引用数据类型
    studentA.getTeacher().setName("张老师");
    System.out.println("studentB:" + studentB.toString());

    }

    studentA:[Student: 1531448569,Teacher:pers.fulsun.Teacher@1e80bfe8:老师1,name:张三]
    studentB:[Student: 1198108795,Teacher:pers.fulsun.Teacher@1e80bfe8:老师1,name:李四]
    studentB:[Student: 1198108795,Teacher:pers.fulsun.Teacher@1e80bfe8:张老师,name:李四]
    • 输出的结果可见,通过 studentA.clone() 拷贝对象后得到的 studentB,和 studentA 是两个不同的对象。
    • studentAstudentB 的基础数据类型的修改互不影响,而引用类型 subject 修改后是会有影响的。
    • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。
    • 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。

深拷贝

  • 可以看到,浅拷贝会带来数据安全方面的隐患,例如我们只是想修改了 studentAname,但是 studentBname 也被修改了,因为它们都是指向的同一个地址。所以,此种情况下,我们需要用到深拷贝。

  • 深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。

深拷贝特点

  • 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。
    • 基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
  • 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。
    • 改变其中一个,不会对另外一个也产生影响。
  • 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。
  • 深拷贝相比于浅拷贝速度较慢并且花销较大。

拷贝的实现

  • Studentclone() 方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。

  • 对于 Student 的引用类型的成员变量 Teacher ,需要实现 Cloneable 并重写 clone() 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Teacher implements Cloneable{
    private String name;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public Teacher(String name) {
    this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    //如果也有引用类型的成员属性,也应该和 Student 类一样实现
    return super.clone();
    }
    }
  • 重写clone()方法,对克隆对象的应用对象进行克隆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Student implements Cloneable {
    @Override
    protected Object clone() {
    try {
    // 浅拷贝:直接调用父类的clone()方法
    Student clone = (Student) super.clone();
    // 对新的对象的引用类型再调用拷贝操作
    clone.teacher = (Teacher) teacher.clone();
    return clone;
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    return null;
    }
    }
    }

String类

  • String类的一些特性

    • String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
    • 字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。
    • Java 语言提供对字符串串联符号(”+”)以及将其他对象转换为字符串的特殊支持。字符串串联是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。
    • 字符串相加:多个常量相加没有性能问题,在编译期就优化了, 常量相加,是先加,然后在常量池找,如果有就直接返回,否则,就创建。
    • 变量与常量相加,会产生多个垃圾对象,变量相加,先开空间,在拼接。
    • String 的演化,Java 9 中底层把 char 数组换成了 byte 数组,占用更少的空间
  • 一旦被创建就不能改变,这样设计有很多好处,比如可以缓存hashcode、使用更加便利以及更加安全等。

    • 在JVM里,考虑到垃圾回收(Garbage Collection)的方便,将heap(堆)划分为三部分:young generation(新生代)、tenured generation (old generation)(旧生代)、permanent generation(永生代)。String字符串缓存 intern()方法,由永久代移到堆中。

    • 字符串为了解决字符串重复问题,生命周期长,存于pergmen中。

    • String直接赋值和使用new的区别

      1
      2
      3
      4
      5
      6
      7
      8
      9
      String str2 = new String(“hello”);
      // 至少创建一个对象,也可能两个。
      // 因为用到new关键字,肯定会在heap中创建一个str2的String对象,它的value是“ABC”。
      // 同时如果这个字符串在java String池里不存在,会在java池里创建这个String对象“ABC”。

      String s = “hello”;
      // 可能创建一个或者不创建对象
      // 如果”ABC”这个字符串在java String池里不存在,会在JVM的字符串池里]创建一个String对象(“ABC”),然后str1指向这个内存地址
      // 无论以后用这种方式创建多少个值为”ABC”的字符串对象,始终只有一个内存地址被分配,之后的都是String的拷贝,Java中称为“字符串驻留”,所有的字符串常量都会在编译之后自动地驻留。

    • String的特点一旦被创建就不能改变【内容不能变,引用可以变】

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public class Test2 {
      public static void main(String[] args) {
      String s1 = new String("ABC");
      String s2 = new String("ABC");
      System.out.println(s1 == s2);//false
      System.out.println(s1.equals(s2));//true
      }
      }

      // 注意: 创建不能给是指内容不能变
      public static void main(String[] args) {
      String s = "hello" ;
      // 其实堆中存在 "hello" "world" "worldjava" 三个字符串
      // 这里将s的引用指向了"worldjava"字符串
      s = "world" + "java";
      System.out.println(s); // worldjava
      }

拼接字符串

使用+拼接

  • 在Java中,拼接字符串最简单的方式就是直接使用符号+来拼接。String字符串拼接通过StringBuilder走中间过程,通过append方法实现。null拼接会变成字符串”null”,程序有大量字符串拼接时,建议考虑直接写StringBuilder实现,就不需要底层new很多临时sb对象了

  • 示例:

    1
    2
    3
    String msg = "hello";
    String msg2 = "Java";
    String hollis = msg + "," + msg2;
  • 这里要特别说明一点,有人把Java中使用+拼接字符串的功能理解为运算符重载

    • 其实并不是,Java是不支持运算符重载的。这其实只是Java提供的一个语法糖
  • 运算符重载:

    • 在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
  • 语法糖:

    • 语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
  • 问答题

    1
    2
    3
    4
    5
    下面这条语句一共创建了多少个对象:String s = “a”+“b”+”c”; 分别都是什么?
    答案:5个对象
    分别是 "a" , "b" , "c" , "ab" , "abc"
    因为字符串的特点是一旦被创建就不能被改变,所有在使用常量进行相加的时候,都是在创建新的字符串对象
    最后在把字符串"abc"这个常量值赋值给引用变量s
  • 代码输出的结果是:”111111222222”,但是它工作原理是怎样的呢?

    1
    2
    3
    4
    String str1 = "111111";
    String str2 = "222222";
    String str = str1 + str2;
    System.out.println(str);
    • 其真正实现的原理是中间通过建立临时的StringBuilder对象,然后调用append方法实现。如何验证呢?
      上述代码文件写在Test.java main方法中,使用javac Test.java编译,在执行javap -verbose Test,可以看到如下信息:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      0: ldc           #2                  // String 111111
      2: astore_1
      3: ldc #3 // String 222222
      5: astore_2
      6: new #4 // class java/lang/StringBuilder
      9: dup
      10: invokespecial #5 // Method java/lang/StringBuilder."":()V
      13: aload_1
      14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: aload_2
      18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_3
      29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      32: return
    • 对于java来说,这段代码原理上应该是:

      1
      2
      3
      4
      5
      6
      7
      String str1 = "111111";
      String str2 = "222222";
      StringBuilder sb = new StringBuilder();
      sb.append(str1);
      sb.append(str2);
      String str = sb.toString();
      System.out.println(str);
    • 如下代码的执行结果是什么?是报错,还是”null222222”

      • 正确答案是:”null222222”。
      1
      2
      3
      4
      String str1 = null;
      String str2 = "222222";
      String str = str1 + str2;
      System.out.println(str);
      • 代码原理如下所示

        • 这段代码,StringBuilder对象append一个null字符串会怎么处理呢,这就要去查看源码了。

        • 然后看super.append(sb),该方法继承了父类的方法,父类为AbstractStringBuilder,再去父类中查看append方法

          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
          26
          27
          28
          29
          30
          31
          32
          33
          34
          String str1 = null;
          StringBuilder sb = new StringBuilder();
          sb.append(str1 );

          public StringBuilder append(StringBuffer sb) {
          super.append(sb);
          return this;
          }


          //看AbstractStringBuilder类中append方法
          @Override
          public AbstractStringBuilder append(CharSequence s) {
          if (s == null)
          return appendNull();
          if (s instanceof String)
          return this.append((String)s);
          if (s instanceof AbstractStringBuilder)
          return this.append((AbstractStringBuilder)s);

          return this.append(s, 0, s.length());
          }

          private AbstractStringBuilder appendNull() {
          int c = count;
          ensureCapacityInternal(c + 4);
          final char[] value = this.value;
          value[c++] = 'n';
          value[c++] = 'u';
          value[c++] = 'l';
          value[c++] = 'l';
          count = c;
          return this;
          }

使用concat拼接

  • 除了使用+拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:

    1
    2
    3
    String country = "china";
    String city = "Shanghai";
    String address = country.concat(",").concat(city);

创建机理

  • 由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。

  • 其运行机制是:创建一个字符串时,首先检查池中是否有值相同的字符串对象

    • 如果有则不需要创建直接从池中刚查找到的对象引用;
    • 如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入池中。
  • 但是,通过new方法创建的String对象是不检查字符串池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入池中。

  • 上述原则只适用于通过直接量给String对象引用赋值的情况。

    1
    2
    String str1 = "123"; //通过直接量赋值方式,放入字符串常量池
    String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池
  • String提供了inter()方法。调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并且返回此池中对象的引用。

构造方法

方法 说明
String() 创建一个内容为空的字符串
String(byte[]) 根据指定的字节数组创建对象
String(byte[],int,int) 根据字节数组的一部分创建对象
String(char[]) 根据指定的字符数组创建对象
String(char[],int,int) 根据字符数组的一部分创建对象
String(String) 根据指定的字符串内容创建对象
String(byte[] bytes, Charset charset) 使用指定的编码构造字符串对象

成员方法

方法名 描述
concat(str) 字符串连接字符串str
length() 判断字符串的长度
equals() 比较二个字符串是否相等,区分大小写
equalsIgnorceCase() 不区分大小写
toUpperCase 转换为大写
toLowerCase 转换为小写
indexOf(String str) 查找 参数 字符串 在原始 字符串中第一次 出现的位置索引,没有找到: 返回 -1
lastIndexOf(String str) 查找 参数 字符串 在原始 字符串中最后一次 出现的位置索引 没有找到: 返回 -1
charAt(index) 获得指定索引的字符
subString(int start) 获取从索引开始截取字符串
subString(int Start ,int end) 截取[start,end)范围的字符串
trim(String str) 去掉字符串的前后空格
replace(old,new) 使用new 替换 old,可以替换空格等特殊字符
startsWith() 判断是否指定的参数开头
endsWith() 判断是否指定的参数结尾
comparreTo(参数对象) 与参数对象比较大小,
相等:0
小:负数,比较的在参考的ASCII前 (根据厂商的不同,值不同)
大:正数,比较的在参考的ASCII前 (根据厂商的不同,值不同)
split(“ “) 用参数/(空格,正则)字符串风格为一个字符串数组
toCharArray() 将字符串转换为字符串数组
contains(String) 指定的参数的字符串在原字符串中是否存在,不存在为false

String和数字转换

  • String – > 数字

    1
    2
    3
    4
    5
    6
    7
    new Ingetger(String str).intValue();
    Integer.parseInt(String str);
    num = Integer.valueof(String);

    // String ---> Integer --->int
    Integer ii = new Integer(str);
    int x = ii.intValue();
  • 数字 –> String

    1
    2
    3
    4
    5
    6
    s = num + "";
    s = String.valueof(num);
    s = Integer.toString(num);
    // int ---> Integer -----> String
    Integer i = new Integer(int number);
    String str1 = i.toString();

String技术点

String类是典型的Immutable类

  • 是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。

字符串设计和实现考量

  • String 是 Immutable 类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。
  • 为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。
  • 这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy。

字符串缓存

  • String 在 Java 6 以后提供了 intern()方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。
  • 在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。

String不可变的好处

  • 可以缓存 hash 值
    • 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
  • String Pool 的需要
    • 如果一个String对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
  • 安全性
    • String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
  • 线程安全
    • String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

StringBuffer与StringBuilder

  • StringBuffer和StringBuilder都实现了AbstractStringBuilder抽象类,拥有几乎一致对外提供的调用接口;
  • 其底层在内存中的存储方式与String相同,都是以一个有序的字符序列(char类型的数组)进行存储,不同点是StringBuffer/StringBuilder对象的值是可以改变的,并且值改变以后,对象引用不会发生改变;
  • 二者对象在构造过程中,首先按照默认大小申请一个字符数组,由于会不断加入新数据,当超过默认大小后,会创建一个更大的数组,并将原先的数组内容复制过来,再丢弃旧的数组。因此,对于较大对象的扩容会涉及大量的内存复制操作,如果能够预先评估大小,可提升性能。

StringBuffer类概述

  • 字符串缓冲区,StringBuffer是一个容器
  • 我们如果对字符串进行拼接操作,每次拼接,都会构建一个新的String对象,既耗时,又浪费空间。 而StringBuffer就可以解决这个问题
  • 线程安全的可变的字符序列 , 安全对应的效率比较低
  • StringBuffer和String的区别

    • String 是不可变的字符序列
    • StringBuffer 是可以的字符序列
  • StringBuffer的内部实现采用字符数组,默认数组的长度为16,超过数组大小时,动态扩充的算法是原来的长度*2+2

    • 所以当我们预知要添加的数据长度时,建议使用带初始化容量的构造方法,来避免动态扩充的次数,从而提高效率。

常用方法

  • 构造方法

    方法名 描述
    public StringBuffer() 构造一个空的StringBuffer对象
    public StringBuffer(String str) 将指定的String变为StringBuffer的内容
    public StringBuffer(CharSequence seq) 接收CharSequence接口的实例
方法名 描述
capacity() 查看缓冲区的大小
char charAt(int index) 方法返回此序列中指定索引处的char值。第一个char值在索引0
indexOf(String str) 查询字符串第一次出现的位置
lastIndexOf(String str) 查询字符串最后出现的位置
append(数据类型 b) 提供了很多的append()方法,用于进行字符串连接
append (char[] ,start , end): 从start位置 插入end个字符
insert (int offser,数据类型 b) 在指定位置上增加一个内容
public StringBuffer replace(int start,int end,String str) 将指定范围的内容替换成其他内容
setCharAt (index,char) 修改指定位置的字符
deleteCharAt(index) 删除指定位置的字符
delete(start,end) 删除索引区间为[start , end)的字符
public String substring(int start) 截取从指定索引处的字符开始,直到此字符串末尾。
public String substring(int start,int end) 截取下标[start,end)范围的字符串
public StringBuffer reverse() 字符串翻转
void trimToSize() StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费。

StringBuffer和String的相互转换

  • String – StringBuffer

    • 通过构造方法
    • 通过append()方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //创建一个String对象
    String str = "Hi Java!";

    //方式一:构造方法
    StringBuffer buffer = new StringBuffer(str);
    System.out.println(buffer);

    //方式二:通过append方法
    StringBuffer buffer2 = new StringBuffer();
    buffer2.append(str);
    System.out.println(buffer2);
  • StringBuffer – String

    • 使用substring方法
    • 通过构造方法
    • 通过toString()方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //创建一个StringBuffer对象
    StringBuffer buffer3 = new StringBuffer();
    buffer3.append("Happy birthday!");

    //方式一:通过构造方法
    String str2 = new String(buffer3);
    System.out.println(str2);

    //方式二:通过toString方法
    String str3 = buffer3.toString();
    System.out.println(str3);

    //方式二:通过substring方法
    String str3 = sb.substring(0);
    // 截取指定长度:String str3 = sb.substring(0,sb.length());
    System.out.println(str3);

StringBuilder类

  • StringBuilder是线程不安全的,在jdk1.5后才添加。其他跟StringBuffer一样;StringBuffer和StringBuilder底层是 char[]数组实现的

  • 一个线程的时候优先采用StringBuilder,因为在大多数实现中,它比StringBuffer要快。

基本类型包装类

  • 为什么会有基本类型包装类

    • 为了对基本数据类型进行更多的操作,更方便的操作,java就针对每一种基本数据类型提供了对应的类类型.
    • 原始数据类型和 Java 泛型并不能配合使用
      • Java 的泛型某种程度上可以算作伪泛型,它完全是一种编译期的技巧,Java 编译期会自动将类型转换为对应的特定类型,这就决定了使用泛型,必须保证相应类型可以转换为Object。
    • 无法高效地表达数据,也不便于表达复杂的数据结构
      • Java 的对象都是引用类型,
      • 如果是一个原始数据类型数组,它在内存里是一段连续的内存
      • 而对象数组则不然,数据存储的是引用,对象往往是分散地存储在堆的不同位置。虽然带来了极大灵活性,但是也导致了数据操作的低效,尤其是无法充分利用现代 CPU 缓存机制。
  • 基本类型和包装类的对应

  • 除了 int 和 char 两者的包装类名变化有些大以外,其余六种基本类型对应的包装类名,都是大写了首字母而已。

1
2
3
4
5
6
7
8
byte           Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

int和Integer的区别

  • Integer是int的包装类,int则是java的一种基本数据类型

  • Integer变量必须实例化后才能使用,而int变量不需要

  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值

  • Integer的默认值是null,int的默认值是0

  • 关于Integer和int的比较:

    • Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。

    • Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

    • 非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

    • 对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100);,而java API中对Integer类型的valueOf的定义如下:

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

      java对于-128127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new

安全问题

  • Java自带的线程安全的基本类型包括:
    • AtomicInteger,
    • AtomicLong,
    • AtomicBoolean,
    • AtomicIntegerArray,
    • AtomicLongArray等

验证int类型是否线程安全

  • 200个线程,每个线程对共享变量 count 进行 50 次 ++ 操作

  • int 作为基本类型,直接存储在内存栈,且对其进行+,-操作以及++,–操作都不是原子操作,都有可能被其他线程抢断,所以不是线程安全。int 用于单线程变量存取,开销小,速度快

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    int count = 0;
    private void startThread() {
    for (int i = 0;i < 200; i++){
    new Thread(new Runnable() {
    @Override
    public void run() {
    for (int k = 0; k < 50; k++){
    count++;
    }
    }
    }).start();
    }
    // 休眠10秒,以确保线程都已启动
    try {
    Thread.sleep(1000*10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }finally {
    Log.e("打印日志----",count+"");
    }
    }

    //期望输出10000,最后输出的是9818
    //注意:打印日志----: 9818

AtomicInteger线程安全版

  • AtomicInteger类中有有一个变量valueOffset,用来描述AtomicInteger类中value的内存位置 。

  • 当需要变量的值改变的时候,先通过get()得到valueOffset位置的值,也即当前value的值.给该值进行增加,并赋给next

  • compareAndSet()比较之前取到的value的值当前有没有改变,若没有改变的话,就将next的值赋给value,倘若和之前的值相比的话发生变化的话,则重新一次循环,直到存取成功,通过这样的方式能够保证该变量是线程安全的

  • value使用了volatile关键字,使得多个线程可以共享变量,使用volatile将使得VM优化失去作用,在线程数特别大时,效率会较低

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    private static AtomicInteger atomicInteger = new AtomicInteger(1);
    static Integer count1 = Integer.valueOf(0);
    private void startThread1() {
    for (int i = 0;i < 200; i++){
    new Thread(new Runnable() {
    @Override
    public void run() {
    for (int k = 0; k < 50; k++){
    // getAndIncrement: 先获得值,再自增1,返回值为自增前的值
    count1 = atomicInteger.getAndIncrement();
    }
    }
    }).start();
    }
    // 休眠10秒,以确保线程都已启动
    try {
    Thread.sleep(1000*10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }finally {
    Log.e("打印日志----",count1+"");
    }
    }

    //期望输出10000,最后输出的是10000
    //注意:打印日志----: 10000



    //AtomicInteger使用了volatile关键字进行修饰,使得该类可以满足线程安全。
    private volatile int value;
    /**
    * Creates a new AtomicInteger with the given initial value.
    *
    * @param initialValue the initial value
    */
    public AtomicInteger(int initialValue) {
    value = initialValue;
    }

自动拆/装箱(1.5以后)

  • 自动装箱是将原始数据类型转换为相应的包装类对象的过程,例如,intInteger

    • 将boolean值转换成Boolean对象
    • byte值转换成Byte对象,
    • char转换成Character对象,
    • float值转换成Float对象,
    • int转换成Integer,
    • long转换成Long,
    • short转换成Short,
  • 自动拆箱则是相反的操作,是将包装类对象转换为基本数据类型的过程。 例如,Integerint

  • 在Java中是自动进行拆箱和自动装箱。 但是,我们可以使用valueOf()xxxValue()等方法将一个外部转换为另一个。

    • 自动装箱时编译器调用valueOf将原始类型值转换成对象

      1
      2
      int a =1;
      Integer integer = Integer.valueOf(a);
    • 自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。

      1
      2
      3
      4
      5
      6
      // 手动拆/装箱
      Double aDouble = Double.valueOf(3.14);
      double v = aDouble.doubleValue();

      Integer iObject = Integer.valueOf(3);
      Int iPrimitive = iObject.intValue()

装箱和拆箱的实现

  • 以Interger类为例,下面看一段代码来了解装箱和拆箱的实现

    1
    2
    3
    4
    5
    6
    public class Main {
    public static void main(String[] args) {
    Integer y = 10;
    int c = i;
    }
    }

何时发生自动装箱和拆箱

  • 只要期望包装器类对象,并且提供原始数据类型,反之亦然,就会发生这种情况。

    • 将原始类型添加到Java中的ArrayList中。
    • 创建参数化类的实例,例如,期望TypeThreadLocal
    • 只要需要,Java就会自动将原始类型转换为对象,并在方法调用中提供另一个原始数据类型。
    • 将基元类型分配给对象类型时。
  • 自动装箱主要发生在两种情况,一是赋值时,另一种是在方法调用的时候。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ArrayList<Integer> intList = new ArrayList<Integer>();
    intList.add(1); //autoboxing - primitive to object
    intList.add(2); //autoboxing

    ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>();
    intLocal.set(4); //autoboxing

    int number = intList.get(0); // unboxing
    int local = intLocal.get(); // unboxing in Java

赋值时

  • 这是最常见的一种情况,在Java 1.5以前我们需要手动地进行转换才行,而现在所有的转换都是由编译器来完成。

  • 代码如下:

    1
    2
    3
    //after java5 可以自动拆/装箱
    Integer iObject = 3; //autobxing - primitive to wrapper conversion
    int iPrimitive = iObject; //unboxing - object to primitive conversion

方法调用时

  • 这是另一个常用的情况,当我们在方法调用时,我们可以传入原始数据值或者对象,同样编译器会帮我们进行转换。

  • 代码如下:

    • show方法接受Integer对象作为参数,当调用show(3)时,会将int值转换成对应的Integer对象,这就是所谓的自动装箱,
    • show方法返回Integer对象,而int result = show(3);中result为int类型,所以这时候发生自动拆箱操作,将show方法的返回的Integer对象转换成int值。
    1
    2
    3
    4
    5
    6
    7
    8
    public static Integer show(Integer iParam){
    System.out.println("autoboxing example - method invocation i: " + iParam);
    return iParam;
    }

    //autoboxing and unboxing in method invocation
    show(3); //autoboxing
    int result = show(3); //unboxing because return type of method is Integer

自动装箱的弊端

  • 在一个循环中进行自动装箱操作的情况,如下面的例子就会创建多余的对象,影响程序的性能。

    1
    2
    3
    4
    Integer sum = 0;
    for(int i=1000; i<5000; i++){
    sum+=i;
    }
  • 上面的代码sum+=i可以看成sum = sum + i,但是+这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下

    1
    2
    int result = sum.intValue() + i;
    Integer sum = new Integer(result);
  • 由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。

重载与自动装箱

  • 当重载遇上自动装箱时,情况会比较有些复杂,可能会让人产生有些困惑。

  • 在1.5之前,value(int)和value(Integer)是完全不相同的方法,开发者不会因为传入是int还是Integer调用哪个方法困惑,

  • 但是由于自动装箱和拆箱的引入,处理重载方法时稍微有点复杂。一个典型的例子就是ArrayList的remove方法,它有remove(index)和remove(Object)两种重载,我们可能会有一点小小的困惑,其实这种困惑是可以验证并解开的,通过下面的例子我们可以看到,当出现这种情况时,不会发生自动装箱操作。仍然是重载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public void test(int num){
    System.out.println("method with primitive argument");

    }

    public void test(Integer num){
    System.out.println("method with wrapper argument");

    }

    //calling overloaded method
    AutoboxingTest autoTest = new AutoboxingTest();
    int value = 3;
    autoTest.test(value); //no autoboxing
    Integer iValue = value;
    autoTest.test(iValue); //no autoboxing

    Output:
    method with primitive argument
    method with wrapper argument

对象相等比较

  • ”==“可以用于原始值进行比较,也可以用于对象进行比较

  • 当用于对象与对象之间比较时,比较的不是对象代表的值,而是检查两个对象是否是同一对象,这个比较过程中没有自动装箱发生。

  • 进行对象值比较不应该使用”==“,而应该使用重写对象对应的equals方法进行值得判断。

    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
    26
    public static void main(String args[]) {

    // Example 1: == 基本类型比较看值
    int i1 = 128;
    int i2 = 128;
    System.out.println("i1==i2 : " + (i1 == i2)); // true

    // Example 2: 基本类型和包装类型比较看值
    Integer num1 = 128;
    int num2 = 128;
    System.out.println("num1 == num2 : " + (num1 == num2)); // true

    // Example 3: 自动装箱后的包装类型比较:-128~127之间看值,大于这个区间看地址值
    Integer obj1 = 1;
    Integer obj2 = 1;
    Integer obj3 = 128;
    Integer obj4 = 128;
    System.out.println("obj1 == obj2 : " + (obj1 == obj2)); // true
    System.out.println("obj3 == obj4 : " + (obj1 == obj2)); // false

    // Example 4: 包装类型比较:看地址值
    Integer one = new Integer(1);
    Integer anotherOne = new Integer(1);
    System.out.println("one == anotherOne : " + (one == anotherOne)); // false

    }

Integer

成员变量

  • 最大值:public static final int MAX_VALUE;
  • 最小值:public static final int MIN_VALUE;

构造函数

  • public Integer(int value)

  • public Integer(String s),注意:这个字符串必须是由数字字符组成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 方式1
    int i = 100;
    Integer ii = new Integer(i);
    System.out.println("ii:" + ii); //ii:100

    // 方式2
    String s = "100";
    // NumberFormatException
    // String s = "abc";
    Integer iii = new Integer(s);
    System.out.println("iii:" + iii); //iii:100

Integer进制转换

  • 10进制转为2进制

    • public static String toBinaryString(int i);
  • 10进制转8进制

    • public static String toOctalString(int i);
  • 10进制转16进制

    • public static String toHexString(int i);
  • 十进制到其他进制(范围:2-36,因为表示的数有0-9,a-z共36个)

    • public static String toString(int i,int radix);
  • 其他进制到十进制

    • public static int parseInt(String s,int radix);
    1
    2
    3
    4
    5
    System.out.println(Integer.parseInt("100", 10));    //100
    System.out.println(Integer.parseInt("100", 2)); //4
    System.out.println(Integer.parseInt("100", 8)); //64
    System.out.println(Integer.parseInt("100", 16)); //256
    System.out.println(Integer.parseInt("100", 23)); //529

成员方法

  • public int intValue()  //把Integer类型转化为Int类型
  • public static int parseInt(String s)  //把String类型转化为Int类型
  • public static int parseInt(String s ,int radix)  //把String类型转化为radix进制
  • public static String toString(int i)  //把Int类型转化为String类型
  • public static Integer toString(int i ,int radix)  10进制转为radix进制
  • public static Integer valueOf(String s)   //把String参数给的值,转化为Integer类型

Integer的值缓存的原理

Java 5 中引入缓存特性

  • 在 Java 5 中,为 Integer 的操作引入了一个新的特性,用来节省内存和提高性能。整型对象在内部实现中通过使用相同的对象引用实现了缓存和重用。

  • 这种 Integer 缓存策略仅在自动装箱(autoboxing)的时候有用,使用构造器创建的 Integer 对象不能被缓存。编译器会在自动装箱过程调用 valueOf() 方法,因此多个Integer实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。

  • new Integer(123) 与 Integer.valueOf(123) 的区别在于:

    • new Integer(123) 每次都会新建一个对象;

    • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      Integer x = new Integer(123);
      Integer y = new Integer(123);
      System.out.println(x == y); // false
      Integer z = Integer.valueOf(123);
      Integer k = Integer.valueOf(123);
      System.out.println(z == k); // true

      Integer m = 123; // 自动装箱
      Integer n = 123; // 自动装箱
      System.out.println(m == n); // true
  • valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

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

IntegerCache类

  • 在创建新的 Integer 对象之前会先在 IntegerCache.cache (是个Integer类型的数组)中查找。有一个专门的 Java 类来负责 Integer 的缓存。

  • 这个类是用来实现缓存支持,并支持 -128 到 127 之间的自动装箱过程。最大值 127 可以通过 JVM 的启动参数 -XX:AutoBoxCacheMax=size 修改。 缓存通过一个 for 循环实现。从小到大的创建尽可能多的整数并存储在一个名为 cache 的整数数组中。这个缓存会在 Integer 类第一次被使用的时候被初始化出来。以后,就可以使用缓存中包含的实例对象,而不是创建一个新的实例(在自动装箱的情况下)。

    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
    26
    27
    28
    29
    30
    31
    32
    33
    private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
    // 高值可由属性配置
    int h = 127;
    String integerCacheHighPropValue =
    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
    try {
    int i = parseInt(integerCacheHighPropValue);
    i = Math.max(i, 127);
    //最大数组大小为Integer.MAX_VALUE
    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
    } catch( NumberFormatException nfe) {
    // 如果无法将属性解析为 int,忽略它。
    }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
    cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
    }

其他整型类型的缓存机制

  • 这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
    • 有 ByteCache 用于缓存 Byte 对象
    • 有 ShortCache 用于缓存 Short 对象
    • 有 LongCache 用于缓存 Long 对象
    • 有 CharacterCache 用于缓存 Character 对象
    • boolean的True,False。Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。

Character类概述

  • Character 类在对象中包装一个基本类型 char 的值<
  • 此外,该类提供了几种方法,以确定字符的类别(小写字母,数字,等等),并将字符从大写转换成小写,反之亦然

构造方法

  • public Character(char value)

    1
    2
    3
    4
    5
    6
    public static void main(String[] args) {
    // 创建对象
    // Character ch = new Character((char) 97);
    Character ch = new Character('a');
    System.out.println("ch:" + ch);
    }

成员方法

  • A:判断给定的字符是否是大写     public static boolean isUpperCase(char ch)

  • B:判断给定的字符是否是小写  public static boolean isLowerCase(char ch)

  • C:判断给定的字符是否是数字字符   public static boolean isDigit(char ch)

  • D:把给定的字符转成大写  public static char toUpperCase(char ch)

  • E:把给定的字符转成小写  public static char toLowerCase(char ch)

  • 代码测试

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public static void main(String[] args) {
    // public static boolean isUpperCase(char ch):判断给定的字符是否是大写字符
    System.out.println("isUpperCase:" + Character.isUpperCase('A'));
    System.out.println("isUpperCase:" + Character.isUpperCase('a'));
    System.out.println("isUpperCase:" + Character.isUpperCase('0'));
    System.out.println("-----------------------------------------");
    // public static boolean isLowerCase(char ch):判断给定的字符是否是小写字符
    System.out.println("isLowerCase:" + Character.isLowerCase('A'));
    System.out.println("isLowerCase:" + Character.isLowerCase('a'));
    System.out.println("isLowerCase:" + Character.isLowerCase('0'));
    System.out.println("-----------------------------------------");
    // public static boolean isDigit(char ch):判断给定的字符是否是数字字符
    System.out.println("isDigit:" + Character.isDigit('A'));
    System.out.println("isDigit:" + Character.isDigit('a'));
    System.out.println("isDigit:" + Character.isDigit('0'));
    System.out.println("-----------------------------------------");
    // public static char toUpperCase(char ch):把给定的字符转换为大写字符
    System.out.println("toUpperCase:" + Character.toUpperCase('A'));
    System.out.println("toUpperCase:" + Character.toUpperCase('a'));
    System.out.println("-----------------------------------------");
    // public static char toLowerCase(char ch):把给定的字符转换为小写字符
    System.out.println("toLowerCase:" + Character.toLowerCase('A'));
    System.out.println("toLowerCase:" + Character.toLowerCase('a'));
    }

    结果:
    isUpperCase:true
    isUpperCase:false
    isUpperCase:false
    -----------------------------------------
    isLowerCase:false
    isLowerCase:true
    isLowerCase:false
    -----------------------------------------
    isDigit:false
    isDigit:false
    isDigit:true
    -----------------------------------------
    toUpperCase:A
    toUpperCase:A
    -----------------------------------------
    toLowerCase:a
    toLowerCase:a

正则表达式

  • 正则表达式,不需要记忆,用的时候去查就可以呢

    • 是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。其实就是一种规则。有自己特殊的应用。
    • 具体使用直接上网搜索
      • 例如,匹配手机号,邮箱,身份证号等等
  • 需求:只能输入数字

    • 常规写法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class Demo2{

      public static void main(String[] args) {
      //只能输入数字
      String str = "124354232";
      char[] arr = str.toCharArray();
      boolean flag = true;
      for(int i = 0 ; i< arr.length ; i++){
      if(!(arr[i]>=48&&arr[i]<=57)){
      flag = false;
      }
      }
      System.out.println(flag?"输入正确":"输出只能是数字");
      }
      }
    • 使用正则表达式:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Demo2{
      public static void main(String[] args) {
      //只能输入数字
      String str = "12435423a2";
      boolean flag = str.matches("[0-9]+");
      System.out.println(flag?"输入正确":"只能输入数字");

      }
      }

正则表达式的符号

符号 说明 表达式
. 任何字符(与行结束符可能匹配也可能不匹配)
\d 数字: [0-9]
\D 非数字: [^0-9]
\s 空白字符: [\t\n\x0B\f\r]
\S 非空白字符: [^\s]
\w 单词字符: [a-zA-Z_0-9]
\W 非单词字符: [^\w]
  • 示例代码

    1
    2
    3
    4
    5
    6
    System.out.println("a".matches("."));
    System.out.println("1".matches("\\d"));
    System.out.println("%".matches("\\D"));
    System.out.println("\r".matches("\\s"));
    System.out.println("^".matches("\\S"));
    System.out.println("a".matches("\\w"));

Greedy 数量词

符号 说明
X? X,一次或一次也没有
X* X,零次或多次
X+ X,一次或多次
X{n} X,恰好n次
X{n,} X,至少n次
X{n,m} X,至少n次,但是不超过m次
  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    System.out.println( "a".matches(".") );
    System.out.println( "a".matches("a") );
    System.out.println("a".matches("a?") );
    System.out.println( "aaa".matches("a*") );
    System.out.println( "".matches("a+") );
    System.out.println( "aaaaa".matches("a{5}") );
    System.out.println( "aaaaaaaaa".matches("a{5,8}") );
    System.out.println( "aaa".matches("a{5,}") );
    System.out.println( "aaaaab".matches("a{5,}") );

范围表示

符号 说明
[abc] a、b 或 c(简单类)
[^abc] 任何字符,除了 a、b 或 c(否定)
[a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围)
[a-d[m-p]] a 到 d 或 m 到 p:[a-dm-p](并集)
[a-z&&[def]] d、e 或 f(交集)
[a-z&&[^bc]] a 到 z,除了 b 和 c:[ad-z](减去)
[a-z&&[^m-p]] a 到 z,而非 m 到 p:[a-lq-z](减去)
  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    System.out.println( "a".matches("[a]") );
    System.out.println( "aa".matches("[a]+") );
    System.out.println( "abc".matches("[abc]{3,}") );
    System.out.println( "abc".matches("[abc]+") );
    System.out.println( "dshfshfu1".matches("[^abc]+") );
    System.out.println( "abcdsaA".matches("[a-z]{5,}") );
    System.out.println( "abcdsaA12".matches("[a-zA-Z]{5,}") );
    System.out.println( "abcdsaA12".matches("[a-zA-Z0-9]{5,}") );
    System.out.println( "abdxyz".matches("[a-c[x-z]]+"));
    System.out.println( "bcbcbc".matches("[a-z&&[b-c]]{5,}"));
    System.out.println( "tretrt".matches("[a-z&&[^b-c]]{5,}"));

定位符

符号 说明
^ 行的开头
$ 行的结尾
\b 单词边界
\B 非单词边界
\A 输入的开头
\G 上一个匹配的结尾
\Z 输入的结尾,仅用于最后的结束符(如果有的话)
\z 输入的结尾
  • 示例代码

    1
    2
    System.out.println("45678".matches("^[^0]\\d+"));
    System.out.println("demo.java".matches("\\w+\\.java$"));

示例

  • 校验QQ号,要求:必须是5~15位数字,0不能开头。

  • 有了正则表达式之后:[1-9][0-9]{4,14}表示是第一位数字是会出现1-9范围之间的其中一个,下来的数字范围会出现在0-9之间,至少出现4次,最多出现14次。

    1
    2
    3
    4
    5
    6
    public static void checkQQ2() {
    String qq = "12345";
    String reg = "[1-9][0-9]{4,14}";
    boolean b = qq.matches(reg);
    System.out.println("b="+b);
    }
  • 匹配是否为一个合法的手机号码。

    1
    2
    3
    4
    5
    6
    public static void checkTel() {
    String tel = "25800001111";
    String reg = "1[35]\\d{9}";//在字符串中,定义正则出现\ 要一对出现。
    boolean b= tel.matches(reg);
    System.out.println(tel+":"+b);
    }

正则切割字符串

  • 根据空格对一段字符串进行切割。

    1
    2
    3
    4
    5
    6
    7
    8
    public static void splitDemo() {
    String str = "aa.bb.cc";
    str = "-1 99 4 23";
    String[] arr = str.split(" +");
    for(String s : arr) {
    System.out.println(s);
    }
    }
  • 根据重叠词进行切割。

    • 注意:为了提高规则复用,用()进行封装,每一个括号都有一个编号,从1开始,为了复用这个规则。可以通过编号来完成该规则的调用。
    • 需要对编号数字进行转义。\1就代表获取1组规则。二个拼接后就是二个以上的任意单词
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static void splitDemo2(){
    String str = "sdqqfgkkkhjppppkl";
    String[] arr = str.split("(.)\\1+");
    for(String s : arr) {
    System.out.println(s);
    }
    }
    // 结果
    sd
    fg
    hj
    kl
  • 字符串中的重叠字替换成单个单词。

    1
    2
    3
    4
    5
    6
    7
    8
    public static void replaceDemo()	{
    String str = "sdaaafghcccjkqqqqqql";
    String s = str.replaceAll("(.)\\1+","$1");//$ 可以获取到该方法中正则实际参数中的某一个存在的组 $组编号即可。
    System.out.println(str+":"+s);
    String nums = "wser127372tyuiopd6226178909876789fghjk";
    String s1 = nums.replaceAll("\\d+","*");
    System.out.println(nums+":"+s1);
    }

Pattern和Matcher类

  • 正则的获取功能需要使用的类

  • 步骤:

    1. 先将正则表达式编译成正则对象。使用的是Pattern类一个静态的方法。compile(regex);
    2. 让正则对象和要操作的字符串相关联,通过matcher方法完成,并返回匹配器对象。
    3. 通过匹配器对象的方法将正则模式作用到字符串上对字符串进行针对性的功能操作
  • 需求:获取由3个字母组成的单词。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void getDemo() {
    String str = "da jia zhu yi le,ming tian bu fang jia,xie xie!";
    //想要获取由3个字母组成的单词。
    //刚才的功能返回的都是一个结果,只有split返回的是数组,但是它是把规则作为分隔符,不会获取符合规则的内容。
    //这时我们要用到一些正则对象。
    String reg = "\\b[a-z]{3}\\b";
    Pattern p = Pattern.compile(reg);
    Matcher m = p.matcher(str);
    while(m.find())
    {
    System.out.println(m.start()+"...."+m.end());
    System.out.println("sub:"+str.substring(m.start(),m.end()));
    System.out.println(m.group());
    }
    // System.out.println(m.find());//将规则对字符串进行匹配查找。
    // System.out.println(m.group());//在使用group方法之前,必须要先找,找到了才可以取。
    }

时间

Date

  • Date:表示特定的瞬间,精确到毫秒。
  • 日期类的时间从为什么是从1970年1月1日
    • 最初计算机操作系统是32位,而时间也是用32位表示。Integer在JAVA内用32位表 示,因此32位能表示的最大值是2147483647。另外1年365天的总秒数是31536000,2147483647/31536000 = 68.1。也就是说32位能表示的最长时间是68年,而实际上到2038年01月19日03时14分07秒,便会到达最大时间,过了这个时间点,所有32位操作系统时间便会变为10000000 00000000 00000000 00000000,也就是1901年12月13日20时45分52秒,这样便会出现时间回归的现象,很多软件便会运行异常了。所以从1970年1月1日开始,延长使用时间。

构造方法:

  • Date():根据当前的默认毫秒值创建日期对象

  • Date(long date):根据给定的毫秒值创建日期对象

  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 创建对象
    Date d = new Date();
    System.out.println("d:" + d); //d:Mon Jul 30 16:21:01 CST 2018

    // 创建对象
    // long time = System.currentTimeMillis();
    long time = 1000 * 60 * 60; // 1小时
    Date d2 = new Date(time);
    System.out.println("d2:" + d2); //d2:Thu Jan 01 08:00:00 CST 1970

成员方法

  • public long getTime():获取时间,以毫秒为单位

  • public void setTime(long time):设置时间

  • 示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建对象
    Date d = new Date();
    // 获取时间
    long time = d.getTime();
    System.out.println(time); //1532939861629
    // System.out.println(System.currentTimeMillis()); //1532939861630

    System.out.println("d:" + d); //d:Mon Jul 30 16:37:41 CST 2018
    // 设置时间
    d.setTime(1000);
    System.out.println("d:" + d); d:Thu Jan 01 08:00:01 CST 1970

Time

1
2
Time time = new Time(date.getTime());
System.out.println(time); //15:20:51

Calendar类

  • 它为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR 等 日历字段之间的转换提供了一些方法,并为操作日历字段(例如获得下星期的日期)提供了一些方法。

  • 月份从0开始

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Calendar c = Calendar.getInstance();
    System.out.println(c.getTime()); //Sun Aug 12 15:23:15 CST 2018
    System.out.println(c.get(Calendar.YEAR)); //2018
    System.out.println(c.get(Calendar.MONTH)+1); //8
    System.out.println(c.get(Calendar.DATE)); /12

    //add()调节时间
    c.add(Calendar.YEAR, -10); //10年前的今天;
    System.out.println(c.getTime()); //Tue Aug 12 15:24:55 CST 2008
  • public final void set(int year,int month,int date):设置当前日历的年月日

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Calendar c = Calendar.getInstance();
    c.set(1314,05,20);
    // 获取年
    year = c.get(Calendar.YEAR);
    // 获取月
    month = c.get(Calendar.MONTH);
    // 获取日
    date = c.get(Calendar.DATE);
    System.out.println(year + "年" + (month + 1) + "月" + date + "日"); //1314年6月20日
  • 获取任意一年的二月有多少天

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /** 分析:
    * A:键盘录入任意的年份
    * B:设置日历对象的年月日
    * 年就是A输入的数据
    * 月是2
    * 日是1
    * C:把时间往前推一天,就是2月的最后一天
    * D:获取这一天输出即可
    */
    public static void main(String[] args) {
    // 键盘录入任意的年份
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入年份:");
    int year = sc.nextInt();

    // 设置日历对象的年月日
    Calendar c = Calendar.getInstance();
    c.set(year, 2, 1); // 其实是这一年的3月1日
    // 把时间往前推一天,就是2月的最后一天
    c.add(Calendar.DATE, -1);

    // 获取这一天输出即可
    System.out.println(c.get(Calendar.DATE));
    }

格式化

数字的格式化

1
2
3
4
5
6
7
// 数字的格式化Format
//当前环境 缺省的格式
NumberFormat f1 = NumberFormat.getInstance();
System.out.println(f1.format(234.55665)); //234.557
// 当前环境缺省的货币格式
f1 = NumberFormat.getCurrencyInstance();
System.out.println(f1.format(2.364588)); //¥2.36

日期格式化

  • DateFormat抽象类:针对日期进行格式化和针对字符串进行解析的类

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    //父类
    //当前环境缺省格式
    DateFormat df = DateFormat.getInstance();
    System.out.println(df.format(date));//18-8-10 上午11:47
    //==Date=============================================================
    df = DateFormat.getDateInstance();
    System.out.println(df.format(date));//2018-8-10

    df = DateFormat.getDateInstance(DateFormat.SHORT);
    System.out.println(df.format(date));//18-8-10

    df = DateFormat.getDateInstance(DateFormat.MEDIUM);
    System.out.println(df.format(date));//2018-8-10

    df = DateFormat.getDateInstance(DateFormat.LONG);
    System.out.println(df.format(date));//2018年8月10日

    df = DateFormat.getDateInstance(DateFormat.FULL);
    System.out.println(df.format(date));//2018年8月10日 星期五
    //==Time=============================================================
    df = DateFormat.getTimeInstance();
    System.out.println(df.format(date));//11:50:46

    d1 = DateFormat.getTimeInstance(DateFormat.SHORT);
    System.out.println(d1.format(date));// 上午11:58

    d1 = DateFormat.getTimeInstance(DateFormat.MEDIUM);
    System.out.println(d1.format(date));// 11:50:46

    d1 = DateFormat.getTimeInstance(DateFormat.LONG);
    System.out.println(d1.format(date));// 上午11时58分30秒
    //===DateTime=============================================================
    df = DateFormat.getDateTimeInstance();
    System.out.println(df.format(date));//2018-8-10 11:51:20

    df = DateFormat.getDateTimeInstance(DateFormat.FULL,DateFormat.FULL);
    System.out.println(df.format(date));//2018年8月10日 星期五 上午11时52分06秒 CST
  • 子类SimpleDateFormat

    • SimpleDateFormat(String pattern):给定的模式构造
      • 年 y
      • 月 M
      • 日 d
      • 时 H
      • 分 m
      • 秒 s
    • public final String format(Date date) 格式化)
    • public Date parse(String source) 解析
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 创建日期对象
    Date d = new Date();
    // 创建格式化对象
    // SimpleDateFormat sdf = new SimpleDateFormat(); //18-7-30 下午5:09(默认格式)
    // 给定模式
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    // public final String format(Date date)
    String s = sdf.format(d);
    System.out.println(s); //2018年07月30日 17:07:17

    String str = "2008-08-08 12:12:12";
    //在把一个字符串解析为日期的时候,请注意格式必须和给定的字符串格式匹配
    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date dd = sdf2.parse(str);
    System.out.println(dd); //Fri Aug 08 12:12:12 CST 2008

jdk8新API

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
26
27
28
29
30
31
32
33
34
35
36
//日期
LocalDate ldate = LocalDate.now();
ldate = LocalDate.of(2012, 1, 23); //自己设置时间
System.out.println(ldate); //2012-02-23
System.out.println(ldate.getYear()); //2012
System.out.println(ldate.getMonthValue()); //2
System.out.println(ldate.getDayOfMonth()); //23
System.out.println(ldate.getDayOfYear()); //一年中的第几天54
System.out.println(ldate.getDayOfWeek()); //THURSDAY
System.out.println(ldate.plusYears(10)); //2022-02-23 10年后
System.out.println(ldate.minusYears(5)); //2007-02-23 5年前

//时间
LocalTime ltime = LocalTime.now();
ltime = LocalTime.of(3, 20, 23);
System.out.println(ltime); //03:20:23
System.out.println(ltime.getHour()); //3
System.out.println(ltime.plusHours(10)); //13:20:23
System.out.println(ltime.minusHours(4)); //23:20:23

//日期时间
LocalDateTime ldt1 = LocalDateTime.now();
ldt1 = LocalDateTime.of(2012, 2, 3, 4, 5 ,56);
System.out.println(ldt1); //2012-02-03T04:05:56
System.out.println(ldt1.getYear()); //2012
System.out.println(ldt1.getDayOfMonth()); //3

//运算
LocalDateTime ldt2 = LocalDateTime.now();
ldt2 = LocalDateTime.of(2012, 1, 3, 4, 5, 56);

LocalDateTime ldt3 = LocalDateTime.now();
ldt3 = LocalDateTime.of(2012, 2, 5, 4, 5, 56);

Duration du = Duration.between(ldt2, ldt3);
System.out.println(du.toDays()); //33

Math类

  • java.lang.Math(不用导包),针对数学的一个类,方法都式static的
  • Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。

成员变量

方法名 描述
public static final double E 自然底数
public static final double PI 圆周率

常用方法

方法名 描述
public static double ceil(double d): 向上取整,获取大于指定参数的最小整数 Math.ceil(33.4) //34.0
public static double floor(double d) 向下取整,获取小于指定参数的最大整数 Math.floor(33.4) //33.0
public static long round(double d) 四舍五入 Math.round(33.5) //34
public static double pow(double a,double b) 获取a的b次幂,a是底b是指数 Math.pow(2,3) //8.0
public static double random() 产生[0.1)的随机小数
(int)(Math.rando*(n-m+1)+m) 产生任意范围内[m,n]的随机数
public static int abs(int a) 取绝对值
public static int max(int a,int b) 获取最大值
public static int min(int a, int b) 获取最小值
public static double sqrt(double a) 获取正平方根

Random类

  • 随机类java.util.Random
    • 此类用于产生随机数如果用相同的种子创建两个 Random 实例,则对每个实例进行相同的方法调用序列,它们将生成并返回相同的数字序列

构造方法

1
2
3
4
public Random()             没有给定种子,使用的是默认的(当前系统的毫秒值)
public Random(long seed) 给定一个long类型的种子,给定以后每一次生成的随机数是相同的
public int nextInt()
public int nextInt(int n)

使用

  • 示例代码如下:

    1
    2
    3
    4
    //创建对象
    Random r = new Random();
    r.nextInt(10);//产生[0,10)之间的随机整理
    r.nextDouble();//产生随机小数[0.1)

System类

  • 系统级类
    • System 类包含一些有用的类字段和方法。它不能被实例化。
    • 系统级操作

类与类的关系

  • 在学习面向对象设计时,类关系涉及依赖、关联、聚合、组合和泛化这五种关系
  • 耦合度依次递增。关于耦合度,可以简单地理解为当一个类发生变更时,对其他类造成的影响程度
  • 影响越小则耦合度越弱,影响越大耦合度越强。

依赖(Dependency)

  • 是一种 使用 的 关系.

    • 学生使用电脑:学生指向电脑
    • 人使用车:人 指向 车
    • 动物依赖食物: 动物指向食物
  • UML建模语言: 虚线 + 箭头

  • A要完成某个功能引用了类B,则类A依赖类B。

    • 依赖在代码中主要体现为类A的某个成员函数的返回值、形参、局部变量或静态方法的调用使用了类B,则表示类A引用了类B

关联(Association)

  • 是一种拥有的关系,类之间的关系比依赖要强。学生可以不用电脑,但是学生不能没有老师。

    • 学员拥有课程
  • 对象拥有的个数分类

    • 单向关联,双向关联、自身关联、多维关联。后三个可以不加箭头。
    • 1对1(汽车 和 车位)
    • 1对多(学员 和 多门课程)
    • 多对多(老师 和 学员)
  • UML建模语言:实线 + 箭头

  • 相似之处:
    关联暗示了依赖,二者都用来表示无法用聚合和组合表示的关系。

  • 区别:

    • 发生依赖关系的两个类都不会增加属性。其中的一个类作为另一个类的方法的参数或者返回值,或者是某个方法的变量而已。

    • 发生关联关系的两个类,类A成为类B的属性,而属性是一种更为紧密的耦合,更为长久的持有关系。

    • 从关系的生命周期来看,

      • 依赖关系是仅当类的方法被调用时而产生,伴随着方法的结束而结束。
      • 关联关系当类实例化的时候产生,当类对象销毁的时候关系结束。
      • 相比依赖,关联关系的生存期更长。

聚合(Aggregation)

  • 聚合是强关联,表示整体和局部,局部离开整体 ,整体可以独立存在(键盘和鼠标)

    • 班级与学生之间存在聚合关系
  • UML建模语言:空心菱形 + 箭头,菱形指向整体

组合(Composition)

  • 组合又叫复合,组合也是聚集的一种。表示整体和局部的关系,局部离开整体整体不可以独立存在(人和心脏)

  • 组合关系中,客户端只认识Student类,根本不知道Heart类的存在,因为心脏类被严密地封装在学生类中。

  • 聚合的成员可独立,复合的成员必须依赖于整体才有意义。

  • UML建模语言: 实心菱形 + 箭头 ,实心菱形指向 整体

泛化(继承)

  • 泛化是学术名称,通俗来讲,泛化指的是类与类之间的继承关系。

  • UML建模语言: 实线 + 空心三角,子类指向 –> 父类

实现(接口)

  • 表示类与接口之间的实现关系

  • UML建模语言: 虚线 + 空心三角,实现类指向 –> 接口

小结

  • 依赖、关联、聚合、组合与泛化代表类与类之间的耦合度依次递增。

    • 依赖关系实际上是一种比较弱的关联
    • 聚合是一种比较强的关联
    • 组合是一种更强的关联
    • 泛化则是一种最强的关联
    • 所以笼统的来区分的话,实际上这五种关系都是关联关系。
  • 依赖关系比较好区分,它是耦合度最弱的一种,在编码中表现为类成员函数的局部变量、形参、返回值或对静态方法的对依赖对象的调用。

  • 关联、聚合与组合在编码形式上都以类成员变量的形式来表示,所以只给出一段代码我们很难判断出是关联、聚合还是组合关系,我们需要从上下文语境中来判别。

    • 关联表示类之间存在联系,不存在集体与个体、个体与组成部分之间的关系。
    • 聚合表示类之间存在集体与个体的关系。
    • 组合表示个体与组成部分之间的关系。
  • 依赖、关联、聚合与组合是逻辑上的关联,泛化和实现是物理上的关联。

    • 物理上的关联指的是类体的耦合,所以类间耦合性最强。