java 复习第一次

类和对象

类间关系

  • 依赖

即使用一个类时需要访问另一个类的对象

e.g. 订单类需要访问账户对象查看信用状态

  • 聚合

类A的对象包含一些类B的对象

  • 继承

子类继承父类

自定义类

属性

  • final实例字段
  • 这样的字段必须在构造对象时初始化并不能再修改。

  • 此修饰符对于类型为基本类型或不可变类的字段尤其有用。

  • 静态字段

    • 将一个字段定义为static,则每个类只有一个这样的字段,即使没有对象,静态字段也存在。(属于类而不属于单个对象)

    • 而对于非静态的实例字段,每个对象都有自己的一个副本。(所有类的对象的这个字段都是一个/类的每个对象都有各自的)

  • 初始化数据字段的方法

    1.在构造器中设置值

    2.在声明中赋值

    3.初始化块: 一个类的声明中可以包含任意多个代码块。只要构造类的对象,这些块就会被执行。

构造器

  • 与类同名

  • 一个类可以有多个构造器(参数不同)(重载)

  • 构造器可以没有参数或有多个参数

  • 没有返回值(其实返回值就是所创建的类的对象)

  • 常与new操作符一起调用

  • 无参数的构造器:创建对象时,对象状态会设置为适当的默认值。

  • 使用this关键字调用同类的另一个构造器

方法

  • 显式参数和隐式参数

    • 在方法名后的括号中声明的是显式参数

    • 在方法中使用的对象的属性(方法调用的目标或接收者)是隐式参数。关键字this指示隐式参数。

  • 静态方法

    调用时并不使用任何类的对象, 没有隐式参数,不能访问对象的实例字段,不能在对象上执行操作。

    可访问静态字段,用类直接调用,静态方法和静态数据成员会随着类的定义而被分配和装载入内存中。

  • 普通方法

    • 既可以访问静态数据成员 又可以访问非静态数据成员

    • 只有在类的对象创建时在对象的内存中才有这个方法的代码段

    • 只能用构造的类的对象调用方法

  • 工厂方法

    • 使用静态工厂方法构造对象

    • 无法命名构造器(构造器的名字必须与类名相同,但希望有两个不同的名字时)

    • 使用构造器时无法改变构造对象的类型

对象

想要使用对象,必须通过构造器构造对象并指定初始状态,再对对象应用方法。

可用var关键字声明局部变量,无需指定类型。

static关键字

静态字段

​ 将一个字段定义为static,则每个类只有一个这样的字段,即使没有对象,静态字段也存在。(属于类而不属于单个对象)而对于 非静态的实例字段,每个对象都有自己的一个副本。(所有类的对象的这个字段都是一个/类的每个对象都有各自的)

静态常量

​ 在类中定义一个静态常量后,可直接用类来调用这个常量。

静态方法

调用时并不使用任何类的对象,没有隐式参数,不能访问对象的实例字段,不能在对象上执行操作。可访问静态字段,用类直接调用。

LocalDate类

  • 使用静态工厂方法构造对象。

LocalDate.now();构造这个对象时的日期

LocalDate.of(YYYY,MM,DD);指定日期

对于已构造的对象可使用getYear,getMonthValue,getDayOfMonth方法得到年月日

使用plusDay(int)方法计算距离当前日期的新日期

只访问对象不更改的成为访问器方法

修改对象的称为修改器方法

java中各种访问器修饰符

Public

​ 修饰的类,类属性变量及方法,包内及包外的任何类(包括子类和普通类)都可以访问。

Private

​ 修饰的类,类属性变量及方法,只有本类可以访问,而在包内包外的任何类都不能访问。

Protect

​ 修饰的类,类属性变量及方法,包内的任何类及包外那些继承了该类的子类才能访问,重点突出继承。

Default

​ 如果一个类,类属性变量及方法没有用任何的修饰符,则其为默认的访问权限Default,默认的访问权限的类,类属性变量及方法,包内的任何类(包括继承了此类的子类)都可以访问他,而对于包外的任何类都不能访问他(包括包外继承了此类的子类),重点突出包。

总结:

​ Protected修饰符修饰的成员变量和方法也称为受保护的成员变量和方法,受保护的成员变量和方法可以再本类或者同一个包中的其他类(包括子类)中通过类的实例进行访问,也可以被同一个包中的类或不同包中的类继承,但是不能再不同包中的其他类(包括子 类)中通过类的实例进行访问。Protected属于子类限制修饰符。

​ Public修饰符修饰的类,那么该类的类名必须与他所在的源文件同名,一个.java源文件中有且只有一个Public类,顶层类只能用Public和默认修饰符(无修饰符)修饰。

​ Protected修饰符修饰的类属性成员变量和方法,只可以被子类访问,而不管子类是不是和父类位于同一个包中。

​ Default修饰符修饰的类属性成员变量和方法,只可以被同一个包中的其他类访问,不管其他类是不是该类的子类。Default属于包限制修饰符。

Java中static方法和普通方法的区别

静态方法

是使用static关键字修饰的方法,又叫类方法。属于类的,不属于对象,在实例化对象之前就可以通过类名.方法名调用静态方法。(静态属性,静态方法都是属于类的,可以直接通过类名调用)。

A.在静态方法中,可以调用静态方法。
B.在静态方法中,不能调用非静态方法。
C.在静态方法中,可以引用类变量(即,static修饰的变量)。
D.在静态方法中,不能引用成员变量(即,没有static修饰的变量)。
E.在静态方法中,不能使用super和this关键字

F.静态方法可以直接调用,类名调用和对象调用。(类名.方法名 / 对象名.方法名)

G.静态方法的生命周期跟相应的类一样长,静态方法和静态变量会随着类的定义而被分配和装载入内存中。一直到线程结束,静态属性和方法才会被销毁。(也就是静态方法属于类)静态方法会随着类的定义而被分配和装载入内存中,编译器只为整个类创建了一个静态变量的副本,也就是只分配一个内存空间,虽然可能有多个实例,但这些实例共享该内存,特别值得注意的是,任何一个对象对静态数据成员的修改,都会影响其它对象。

非静态方法

是不含有static关键字修饰的普通方法,又称为实例方法,成员方法。属于对象的,不属于类的。(成员属性,成员方法是属于对象的,必须通过new关键字创建对象后,再通过对象调用)。

A.在普通方法中,可以调用普通方法。
B.在普通方法中,可以调用静态方法
C.在普通方法中,可以引用类变量和成员变量
D.在普通方法中,可以使用super和this关键字

E.非静态方法的生命周期和类的实例化对象一样长,只有当类实例化了一个对象,非静态方法才会被创建,而当这个对象被销毁时,非静态方法也马上被销毁。(也就是非静态方法属于对象)。静态不能引用非静态这一特性,是由于静态的会随着类的定义而被分配和装载入内存中这一关键点决定的;如果静态引用了非静态的,根本无法从内存中找到非静态的代码段,势必会出错,这种做法是Java虚拟机决不允许的

F.但是非静态方法只能通过对象调用。(对象名.方法名)

总结:类方法可以直接通过类名调用,实例方法必需先实例化类,再初始化对象,然后通过类的实例对象才能调用

String 类

一、String简介

1.1、String(字符串常量)概述

  在API中是这样描述:

    String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
    字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。

  java.lang.String:

    

1.2、分析String源码

1)String的成员变量
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
 /** String的属性值 */  
private final char value[];

/** The offset is the first index of the storage that is used. */
/**数组被使用的开始位置**/
private final int offset;

/** The count is the number of characters in the String. */
/**String中元素的个数**/
private final int count;

/** Cache the hash code for the string */
/**String类型的hash值**/
private int hash; // Default to 0

/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/

  private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];

    从源码看出String底层使用一个字符数组来维护的。

    成员变量可以知道String类的值是final类型的,不能被改变的,所以只要一个值改变就会生成一个新的String类型对象,存储String数据也不一定从数组的第0个元素开始的,而是从offset所指的元素开始。

2)String的构造方法
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
String() 
初始化一个新创建的 String 对象,使其表示一个空字符序列。
String(byte[] bytes)
通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
String(byte[] bytes, Charset charset)
通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
String(byte[] bytes, int offset, int length)
通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。
String(byte[] bytes, int offset, int length, Charset charset)
通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 String。
String(byte[] bytes, int offset, int length, String charsetName)
通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 String。
String(byte[] bytes, String charsetName)
通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
String(char[] value)
分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。
String(char[] value, int offset, int count)
分配一个新的 String,它包含取自字符数组参数一个子数组的字符。
String(int[] codePoints, int offset, int count)
分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。
String(String original)
初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
String(StringBuffer buffer)
分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。
String(StringBuilder builder)
分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。

二、创建字符串对象两种方式的区别

2.1、直接赋值方式创建对象

  直接赋值方式创建对象是在方法区的常量池

1
String str="hello";//直接赋值的方式

2.2、通过构造方法创建字符串对象

  通过构造方法创建字符串对象是在堆内存

1
String str=new String("hello");//实例化的方式

2.3、两种实例化方式的比较

1)编写代码比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestString {
public static void main(String[] args) {
String str1 = "Lance";
String str2 = new String("Lance");
String str3 = str2; //引用传递,str3直接指向st2的堆内存地址
String str4 = "Lance";
/**
* ==:
* 基本数据类型:比较的是基本数据类型的值是否相同
* 引用数据类型:比较的是引用数据类型的地址值是否相同
* 所以在这里的话:String类对象==比较,比较的是地址,而不是内容
*/
System.out.println(str1==str2);//false
System.out.println(str1==str3);//false
System.out.println(str3==str2);//true
System.out.println(str1==str4);//true
}

}
2)内存图分析

可能这里还是不够明显,构造方法实例化方式的内存图:String str = new String(“Hello”);

首先:

    

当我们再一次的new一个String对象时:

      

3)字符串常量池

      在字符串中,如果采用直接赋值的方式(String str=”Lance”)进行对象的实例化,则会将匿名对象“Lance”放入对象池,每当下一次对不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象,

      这样,所有直接赋值的String对象,如果利用相同的“Lance”,则String对象==返回true;

      比如:对象手工入池

1
2
3
4
5
6
7
public class TestString {
public static void main(String args[]){
String str =new String("Lance").intern();//对匿名对象"hello"进行手工入池操作
String str1="Lance";
System.out.println(str==str1);//true
}
}
4)总结:两种实例化方式的区别

      1)直接赋值(String str = “hello”):只开辟一块堆内存空间,并且会自动入池,不会产生垃圾。

      2)构造方法(String str= new String(“hello”);):会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过public String intern();方法进行手工入池。

        在开发的过程中不会采用构造方法进行字符串的实例化。

    5)避免空指向

      首先了解: == 和public boolean equals()比较字符串的区别

      ==在对字符串比较的时候,对比的是内存地址,而equals比较的是字符串内容,在开发的过程中,equals()通过接受参数,可以避免空指向。

      举例:

1
2
3
4
5
6
7
      String str = null;
      if(str.equals("hello")){//此时会出现空指向异常
        ...
      }
      if("hello".equals(str)){//此时equals会处理null值,可以避免空指向异常
         ...
      }

   6)String类对象一旦声明则不可以改变;而改变的只是地址,原来的字符串还是存在的,并且产生垃圾

     

三、String常用的方法

  

3.1、String的判断功能

1)常用方法
1
2
3
4
  boolean equals(Object obj):比较字符串的内容是否相同
  boolean equalsIgnoreCase(String str): 比较字符串的内容是否相同,忽略大小写
  boolean startsWith(String str): 判断字符串对象是否以指定的str开头
  boolean endsWith(String str): 判断字符串对象是否以指定的str结尾
2)代码测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestString {
public static void main(String[] args) {
// 创建字符串对象
String s1 = "hello";
String s2 = "hello";
String s3 = "Hello";

// boolean equals(Object obj):比较字符串的内容是否相同
System.out.println(s1.equals(s2));
System.out.println(s1.equals(s3));
System.out.println("-----------");

// boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
System.out.println(s1.equalsIgnoreCase(s2));
System.out.println(s1.equalsIgnoreCase(s3));
System.out.println("-----------");

// boolean startsWith(String str):判断字符串对象是否以指定的str开头
System.out.println(s1.startsWith("he"));
System.out.println(s1.startsWith("ll"));
}

}

    结果:

1
2
3
4
5
6
7
8
true
false
------------
true
true
------------
true
false

3.2、String类的获取功能

1)常用方法
1
2
3
4
5
int length():获取字符串的长度,其实也就是字符个数
char charAt(int index):获取指定索引处的字符
int indexOf(String str):获取str在字符串对象中第一次出现的索引
String substring(int start):从start开始截取字符串
String substring(int start,int end):从start开始,到end结束截取字符串。包括start,不包括end
2)代码测试
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
public class TestString {
public static void main(String[] args) {
// 创建字符串对象
String s = "helloworld";

// int length():获取字符串的长度,其实也就是字符个数
System.out.println(s.length());
System.out.println("--------");

// char charAt(int index):获取指定索引处的字符
System.out.println(s.charAt(0));
System.out.println(s.charAt(1));
System.out.println("--------");

// int indexOf(String str):获取str在字符串对象中第一次出现的索引
System.out.println(s.indexOf("l"));
System.out.println(s.indexOf("owo"));
System.out.println(s.indexOf("ak"));
System.out.println("--------");

// String substring(int start):从start开始截取字符串
System.out.println(s.substring(0));
System.out.println(s.substring(5));
System.out.println("--------");

// String substring(int start,int end):从start开始,到end结束截取字符串
System.out.println(s.substring(0, s.length()));
System.out.println(s.substring(3, 8));

}

}

  结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
10
----
h
e
----
2
4
-1
----
helloworld
world
----
helloworld
lowor

3.3、String的转换功能

1)常用方法
1
2
3
  char[] toCharArray():把字符串转换为字符数组
  String toLowerCase():把字符串转换为小写字符串
  String toUpperCase():把字符串转换为大写字符串
2)核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestString {
public static void main(String[] args) {
// 创建字符串对象
String s = "abcde";

// char[] toCharArray():把字符串转换为字符数组
char[] chs = s.toCharArray();
for (int x = 0; x < chs.length; x++) {
System.out.println(chs[x]);
}

System.out.println("-----------");

// String toLowerCase():把字符串转换为小写字符串
System.out.println("HelloWorld".toLowerCase());
// String toUpperCase():把字符串转换为大写字符串
System.out.println("HelloWorld".toUpperCase());

}

  结果:

1
2
3
4
5
6
7
8
a
b
c
d
e
-----
helloworld
HELLOWORLD

  注意:  

    字符串的遍历有两种方式:一是ength()加上charAt()。二是把字符串转换为字符数组,然后遍历数组。

3.4、其他常用方法

1)常用方法
1
2
  去除字符串两端空格:String trim()
  按照指定符号分割字符串:String[] split(String str)
2)核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestString {
public static void main(String[] args) {
// 创建字符串对象
String s1 = "helloworld";
String s2 = " helloworld ";
String s3 = " hello world ";
System.out.println("---" + s1 + "---");
System.out.println("---" + s1.trim() + "---");
System.out.println("---" + s2 + "---");
System.out.println("---" + s2.trim() + "---");
System.out.println("---" + s3 + "---");
System.out.println("---" + s3.trim() + "---");
System.out.println("-------------------");

// String[] split(String str)
// 创建字符串对象
String s4 = "aa,bb,cc";
String[] strArray = s4.split(",");
for (int x = 0; x < strArray.length; x++) {
System.out.println(strArray[x]);
}
}

}

  结果:

1
2
3
4
5
6
7
8
9
10
---helloworld---
---helloworld---
--- helloworld ---
---helloworld---
--- hello world ---
---hello world---
--------------------
aa
bb
cc

四、String的不可变性

当我们去阅读源代码的时候,会发现有这样的一句话:

意思就是说:String是个常量,从一出生就注定不可变。

我想大家应该就知道为什么String不可变了,String类被final修饰,官方注释说明创建后不能被改变,但是为什么String要使用final修饰呢? 

4.1、前言

  了解一个经典的面试题:

1
2
3
4
5
6
7
8
9
10
11
public class Apple {
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println(a==b); //true
System.out.println(a.equals(b)); //true
System.out.println(a==c); //false
System.out.println(a.equals(c)); //true
}
}

  内存图:

    

4.2、分析

  因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,

  就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化。  

  需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么a.equals(c)返回true的原因了。

4.3、String不可变的好处

  可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。

  我们的程序中大量使用了String字符串,有可能是出于安全性考虑。

  大家都知道HashMap中key为String类型,如果可变将变的多么可怕。

  当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

五、字符串常量池

5.1、字符串常量池概述

1)常量池表(Constant_Pool table)

    Class文件中存储所有常量(包括字符串)的table。
    这是Class文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是Class文件中的字节码指令。

2)运行时常量池(Runtime Constant Pool)

    JVM内存中方法区的一部分,这是运行时的内容
    这部分内容(绝大部分)是随着JVM运行时候,从常量池转化而来,每个Class对应一个运行时常量池
    上一句中说绝大部分是因为:除了 Class中常量池内容,还可能包括动态生成并加入这里的内容

3)字符串常量池(String Pool)

    这部分也在方法区中,但与Runtime Constant Pool不是一个概念,String Pool是JVM实例全局共享的,全局只有一个
    JVM规范要求进入这里的String实例叫“被驻留的interned string”,各个JVM可以有不同的实现,HotSpot是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留。

5.2、享元模式

  其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义 - - - > 共享元素模式
  也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素
  Java中String部分就是根据享元模式设计的,而那个存储元素的地方就叫做“字符串常量池 - String Pool”

5.3、详细分析

  举例:

1
2
int x  = 10;
String y = "hello";

  1)首先,10"hello"会在经过javac(或者其他编译器)编译过后变为Class文件中constant_pool table的内容

  2)当我们的程序运行时,也就是说JVM运行时,每个Classconstant_pool table中的内容会被加载到JVM内存中的方法区中各自Class的Runtime Constant Pool。

  3)一个没有被String Pool包含的Runtime Constant Pool中的字符串(这里是”hello”)会被加入到String Pool中(HosSpot使用hashtable引用方式),步骤如下:   

    一是:在Java Heap中根据”hello”字面量create一个字符串对象
    二是:将字面量”hello”与字符串对象的引用在hashtable中关联起来,键 - 值 形式是:”hello” = 对象的引用地址。

   另外来说,当一个新的字符串出现在Runtime Constant Pool中时怎么判断需不需要在Java Heap中创建新对象呢?

  策略是这样:会先去根据equals来比较Runtime Constant Pool中的这个字符串是否和String Pool中某一个是相等的(也就是找是否已经存在),如果有那么就不创建,直接使用其引用;反之,如上3

  如此,就实现了享元模式,提高的内存利用效率。

  举例:

      使用String s = new String(“hello”);会创建几个对象

      会创建2个对象

      首先,出现了字面量”hello”,那么去String Pool中查找是否有相同字符串存在,因为程序就这一行代码所以肯定没有,那么就在Java Heap中用字面量”hello”首先创建1个String对象。

      接着,new String(“hello”),关键字new又在Java Heap中创建了1个对象,然后调用接收String参数的构造器进行了初始化。最终s的引用是这个String对象.