Java基础

由于我是中途写的,所以我不知道标号是啥了,所以就先这么写吧

第二章

访问修饰符

面向对象三大基本

封装

继承

super

多态

好处:提高代码复用性,有利于代码的维护

多态是建立在封装和继承的基础之上的

方法的多态

重写和重载就体现了多态

对象的多态

  • 一个对象的编译类型和运行类型可以不一致
1
2
Animal animal =new Dog();
//Animal是父类,Dog是子类

animal的编译类型是Animal,运行类型是Dog。将父类的引用指向子类的对象

  • 编译类型在定义对象时就确定了,不能改变

  • 运行类型是能改变的

1
2
Animal animal =new Dog();
animal =new Cat();

运行类型变为了Cat,编译类型仍然是Animal

  • 编译类型看等号的左边,运行类型看等号的右边

案例1

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 Animal {
public void cry(){
System.out.println("动物在叫唤");
}
}
public class Dog extends Animal {
public void cry(){
System.out.println("小狗狗叫");
}
}
public class Cat extends Animal {
public void cry(){
System.out.println("小猫猫叫");
}
}
public class test {
public static void main(String[] args){
Animal animal=new Dog();
animal.cry();
animal=new Cat();
animal.cry();//重新指向
}
}

这时候先输出狗叫,后输出猫叫

案例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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//改进前
public class Animal{
private String name;
public Animal(String name){
this.name=name;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
}
public class Food{
private String name;
public Food(String name){
this name=name;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
}
public class Master{
private String name;
public Master(String name){
this.name=name;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
public void feed(Dog dog,Bone bone){
System.out.println("主人"+name+"给"+dog.getName()+"喂"+bone.getName());
}
}
public class Dog extends Animal{
public Dog(String name){
super(name);
}
}
public class Bone extends Food{
public Bone(String name){
super(name);
}
}
public class Poly{
public static void main(String[] args){
Master master=Master("tom");
Dog dog=Dog("大黄");
Bone bone=Bone("骨头");
master.feed(dog,bone);
}
}

对于改进方法,由于可能有多个动物,如果每一个动物都编写一个方法,显然过于繁琐。因此可以采用多态的方法来改进,代码如下

1
2
3
4
//将feed方法这么改
public void feed(Animal animal,Food food){
System.out.println("主人"+name+"给"+animal.getName()+"喂"+food.getName());
}

animal的编译类型是Animal,可以接受Animal子类的对象,food同理,因此在主方法里的dog和bone都可以传给animal和food

多态的注意事项

  • 多态的前提是两个对象存在继承关系

  • 向上转型,也叫做自动类型转换(父类的引用指向子类的对象)

    可以调用父类的所有成员(属性和方法),但不能调用子类的特有成员。因为在编译阶段,能调用哪些成员是由编译类型来决定的

    最终运行效果看子类的具体实现,即调用方法时,按照从子类开始查找方法,然后调用

  • 向下转型,也叫做强制类型转换(将父类引用强制转换为子类引用)

    只能强转父类的引用,不能强转父类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class A {
public void Methoda(){
System.out.println("a被调用");
}
}
public class B extends A{
public void Methodb(){
System.out.println("b被调用");
}
}
public class test {
public static void main(String[] args) {
A a=new B();
a.Methoda();
//a.Methodb();这句话错误
}
}

    方法b不在编译类型A里,因此找不到方法b,报错

    解决办法:向下转型(公式:子类类型 引用名 = (子类类型) 父类引用)

1
2
3
A a=new B();
B b=(B) a;
b.Methodb();

    也许读到这你就会疑惑了,为什么我不能直接 B b=new B();向下转型岂不是多此一举吗?

    每一种方法的创造必然有它的价值,向下转型亦如此。其实这么做是为了泛型编程而考虑的。这里由于知识匮乏,暂时找不出例子以及实现原理,待后续补充

  • 属性没有重写之说

    属性的值看编译类型

  • instanceof比较操作符

    java的保留关键字,测试左边的对象是否为右边的类或者其子类型,返回boolen数据类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class test {
    public static void main(String[] args) {
    B b=new B();
    System.out.println(b instanceof B);
    System.out.println(b instanceof A);
    System.out.println(b instanceof Object);
    A a=new B();
    System.out.println(a instanceof B);
    System.out.println(a instanceof A);
    System.out.println(a instanceof Object);
    Object object=new Object();
    System.out.println(object instanceof A);
    }
    }
    class A{
    }
    class B extends A{
    }

    这里空写了两个类,B继承A。对于b的编译类型属于A以及Object的子类型,前三个返回true。同理,中间三个也是返回true。对于最后一个,object的编译类型时Object不属于A的子类型,因此返回false

    案例3

1
2
Object obj=new Integer(9);
String str=(String)obj;

运行该段代码会抛出异常java.lang.Integer cannot be cast to java.lang.String什么意思呢,是指int类型不能强转为String类型,这里属于向下转型,但是Integar与String不在同一个对象阶层,强制转换只会在同一个对象阶层转化

比如这里的B和C不属于同一阶层不能强制转换,而C和D属于同一阶层,可以强制转换

动态绑定机制

  • 当调用对象方法时,该方法会和该对象的内存地址(运行类型)绑定

  • 当调用对象属性时,没有动态绑定机制,哪里声明,哪里使用

这两句话什么意思呢,下面看代码解读:

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
public class A {
public int i=10;
public int sum(){
return getl()+10;
}
public int sum1(){
return i+10;
}
public int getl(){
return i;
}
}
public class B extends A {
public int i=20;
public int getl(){
return i;
}
public int sum1(){
return i+10;
}
}
public class test {
public static void main(String[] args) {
A a=new B();
System.out.print(a.sum());
System.out.println(a.sum1());
}
}
//运行结果:30 30

首先,a的运行类型是B,第一句话中的a.sum在B中没有此类方法,因此要去父类中找,于是跳转到A中的sum方法,但A中的sum方法的返回值中含有getl方法。我们看,A和B类中都有getl方法,这里是用到了方法重写,那么接下来程序会跳转到那个类的getl方法呢?显然是B中的,上文说过:一个方法会和该对象的运行类型绑定,a的运行类型是B,于是就调用了B类的getl,而B类中getl的i是属性,在B中声明为20,就使用B中给的值

上述程序很好的体现了上文所说的两句话

多态的应用

  • 多态数组

数组的定义类型为父类类型,里面保存的实际元素类型为子类类型

实现下列功能:创建Person、Student和Teacher对象,统一放在数组中,并调用say方法

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class Person {
private String name;
private int age;
public Person(String name,int age){
this.name=name;
this.age=age;
}
public void setAge(int age){
this.age=age;
}
public void setName(String name){
this.name=name;
}
public int getAge(){
return age;
}
public String getName(){
return name;
}
public String say(){
return "名字是"+name+" age="+age;
}
}
public class Student extends Person{
private int score;
public Student(String name,int age,int score){
super(name,age);
this.score=score;
}
public void setScore(int score){
this.score=score;
}
public int getScore(){
return score;
}
public String say(){
return super.say()+" score="+score;
}
}
public class Teacher extends Person{
private int salary;
public Teacher(String name,int age,int salary){
super(name,age);
this.salary=salary;
}
public void setSalary(int salary){
this.salary=salary;
}
public int getSalary(){
return salary;
}
public String say(){
return super.say()+" salary="+salary;
}
}
public class test {
public static void main(String[] args) {
int i;
Person[] persons=new Person[5];
persons[0]= new Person("sb1",10);
persons[1]= new Student("sb2",20,100);
persons[2]= new Student("sb3",18,90);
persons[3]= new Teacher("sb4",50,10000);
persons[4]= new Teacher("sb5",40,20000);
for(i=0;i<persons.length;i++){
System.out.println(persons[i].say());
}
}
}

其实,上述在调用say方法的时候启用了动态绑定,person[i]的编译类型为Person,而运行类型根据实际情况来看,new后面为什么就调用什么类里的方法

那么我们怎么调用子类中特有的方法呢,比如在Teacher里加一个teach方法,Student里加一个study方法

1
2
3
4
5
6
public void teach(){
System.out.println(getName()+"在授课");
}
public void study(){
System.out.println(getName()+"在听课");
}

但是我们如果直接使用persons[i].teach或者persons[i].study,则会报错。我们看Person[] persons=new Person[5]这句话代表persons[i]的运行类型为Person,而在Person类里没有teach和study方法,编译器找不到,因此报错。

解决办法:向下转型。

1
2
3
4
5
6
7
8
9
10
11
12
if(persons[i] instanceof Student){
((Student)persons[i]).study();
//注意强转外面的括号不能忽略了,(Student)persons[i].study();是错误的写法
}
else if(persons[i] instanceof Teacher){
((Teacher)persons[i]).teach();
}
else if(persons[i] instanceof Person){
}
else{
System.out.println("类型有误");
}

这里需要加一个判断,因为不知道persons的运行类型具体是什么,如果直接盲目强转会抛出异常。此外,对于Person的判断语句是一个空语句,没有实际操作。

  • 多态参数

下面来看一个案例,能够很好地体现多态参数的含义

定义员工,普通员工和经理类。员工为父类,包含姓名和月工资,以及计算年工资的方法,经理类多了奖金属性和管理方法,普通员工类多了工作方法。

此外还有一个测试类,添加获取年工资的方法和判断员工种类的方法。

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class Employee {
private String name;
private int salary_month;
public Employee(String name,int salary_month){
this.name=name;
this.salary_month=salary_month;
}
public void setName(){
this.name=name;
}
public String getName(){
return name;
}
public void setSalary_month(int salary){
this.salary_month=salary;
}
public int getSalary_month(){
return salary_month;
}
public int getAnnual(){
return 12*salary_month;//年工资等于月工资乘12
}
}
public class Com_Employee extends Employee{
public Com_Employee(String name,int salary){
super(name,salary);
}
public void work(){
System.out.println(getName()+"正在工作");
}
public int getAnnual(){
return super.getAnnual();
}
}
public class Manager extends Employee{
private int bonus;
public Manager(String name,int salary,int bonus){
super(name,salary);
this.bonus=bonus;
}
public void manage(){
System.out.println(getName()+"正在管理");
}
public int getAnnual(){
return super.getAnnual()+bonus;
}
}
public class test {
public static void main(String[] args) {
test t=new test();//这句话容易忽略,容易写成test.方法();
Employee employee1=new Com_Employee("小A",1000);
Employee employee2=new Com_Employee("小B",5000);
Employee employee3=new Manager("小C",10000,200);
System.out.println(t.showEmpAnnual(employee1));
t.testWork(employee1);
System.out.println(t.showEmpAnnual(employee2));
t.testWork(employee2);
System.out.println(t.showEmpAnnual(employee3));
t.testWork(employee3);
}
public int showEmpAnnual(Employee e){
return e.getAnnual();//动态绑定机制
}
public void testWork(Employee e){
if(e instanceof Manager){
((Manager)e).manage();
}
else if(e instanceof Com_Employee){
((Com_Employee)e).work();
}
else{
System.out.println("error");
}
}
}

这里注意,testwork和show方法内涵不同,前者是判断传入的实参到底为何种类型,进行向下转型,而后者为动态绑定机制,getAnnual取决于传入的实参的运行类型。

Object类的几个简单方法

Equals

Equals简介

equals方法是Object超类中的一个方法,默认情况下,比较内存地址是否相等

1
2
3
4
5
//定义一个A类
A a=new A();
A b=new A();
System.out.println(a.equals(b));//false
System.out.println(a==b);//false

如上,new了两个A类对象,因此地址元素不同,故返回false

如果将第二行语句改为A b=a;这时候返回true,两者内存地址相等

实际情况下,一般会重写equals方法,如String类的equals方法比较的是两个对象的内容,而不是内存地址

1
2
3
String a=new String("aaa");
String b=new String("aaa");
System.out.println(a.equals(b));//true

虽然new了两个String对象,但是由于String类的equals方法被重写,因此比较内容,返回true

Equals与==的区别

  • ==主要用于基本类型之间的比较(int,char,boolen……)

    当用于对象之间的比较是,主要以地址是否相等为判断标准

  • equals主要用于对象之间的比较,可以通过重写equals方法来比较内容是否相同

值得注意的是,基本数据类型不能使用equals用来判断

hashCode

返回该对象的哈希码值,提高哈希表的性能,这里仅做一个简单的介绍

  • 两个引用如果指向的是同一个对象,则哈希值肯定是一样的

  • 两个引用如果指向的是不同对象,则哈希值是不一样的

  • 哈希值主要根据地址号来的,不能将哈希值等价于地址

toString

返回类名@哈希值的十六进制

finalize

1
2
A a=new A();
a=null;

这里的a对象就是一个垃圾对象,垃圾回收器就会回收(销毁)对象,将其空间释放,供别的对象使用。

当然,也可以重写finalize方法,实现自己的逻辑,举个简单的重写例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Finalize {
public static void main(String[] args) {
A a=new A();
a=null;
//System.gc();
System.out.println("程序结束");
}
}
class A{
int num;
protected void finalize() throws Throwable {
System.out.println("销毁");
System.out.println("释放……");
}
}

我们在重写的finalize方法里写了两句话,当对象被销毁时,则会输出这两句话,而当我们运行代码的时候发现它并没有输出,这是怎么回事呢?跟算法有关,并不是说某个对象一变成垃圾就会被回收。有没有某种方式能够直接体现这种销毁的思想呢,当然有,在主方法里加一句System.gc();这句话表示运行垃圾回收器

                        运行结果:程序结束
                        销毁
                        释放……              

这里肯定又有人疑惑了,明明System.gc()代码在结束先,不是应该先输入finalize方法里的语句吗?关于这个疑惑,咱后续再讲

在实际开发中,几乎用不到finalize,更多的是为了应付面试

断点

第三章(面向对象高级部分)

类变量和类方法

类变量

定义

类变量也叫静态变量, 定义在类中函数体之外的变量,可以被该类的所有对象实例共享,有static修饰。不加static修饰的称为实例变量/普通变量/非静态变量/非静态成员变量

1
2
3
4
5
6
7
8
9
10
11
12
public class test01 {
public static void main(String[] args) {
A a=new A();
A b=new A();
a.sum++;
b.sum++;
System.out.println(a.sum+" "+b.sum);
}
}
public class A {
static int sum=0;
}

如上,a和b均为A类的实例,而sum为类变量,a和b共享,因此a.sum++;

1
b.sum++;`语句均使用一个sum,输出结果为`2 2

内存

类变量在类加载的时候就生成了

(未完待续)

访问

对于上面的例子,还可以通过A.sum来访问类变量,即使没有创建对象实例。前提是满足访问修饰符的访问权限和范围。

细节

  • 实例对象不能通过类名.变量名访问
  • 只要类加载了就能用类变量了
  • 类变量的生命周期是随类的加载开始,随类的消亡而销毁

类方法

类方法的定义跟前面类变量定义同理

具体使用场景

当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率。不创建实例也可以调用某个方法,如开发自己的工具类时,可以将方法做成静态的,方便调用

细节

  • 类方法中不允许使用和对象有关的关键字,比如thissuper
  • 类方法只能访问静态变量或静态成员
  • 普通成员方法,既可以访问非静态成员,也可以访问静态成员

main方法语法

  • main方法是java虚拟机调用的,所以该方法必须得是public修饰
  • java虚拟机在执行main方法时无需创建对象,所以该方法必须是static
  • main方法接收string类型的数组参数,该数组保存执行java命令时传递给所运行的类的参数

例如,我们可以通过下列方式来遍历该数组

1
2
3
4
5
6
7
8
public class A {
public static void main(String[] args) {
int i;
for(i=0;i<args.length;i++){
System.out.println(args[i]);
}
}
}
  • 在main方法中,我们可以直接调用main方法所在类的静态属性和静态方法,但是不能直接访问该类中的非静态成员,必须创建该类的一个实例对象,才能通过这个对象去访问该类的非静态成员

代码块

定义

代码块又称为初始化块,属于类中的成员,类似方法,将逻辑语句封装在方法体中,通过{ }包围起来

但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。

语法

1
[修饰符]{

代码

1
};
  • 修饰符可写可不写,写的话只能写static
  • static修饰的叫静态代码块,否则叫作普通(非静态)代码块
  • 分号可写可不写

好处

相当于对构造器的补充,用来初始化的操作,如果多个构造器中都有重复的语句,可以加到初始化块中,提高代码的复用性

使用方法

把相同的语句放在一个代码块中,这样当我们调用构造器的时候,创建对象都会先调用代码块的内容

注意事项

  • 静态代码块只会执行一次,而普通代码块每创建一个对象就执行一次
  • 类在什么时候被加载

1.创建对象实例

2.创建子类对象实例,父类会被加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AA {
{
System.out.println("父类代码块");
}
}
public class BB extends AA{
{
System.out.println("子类代码块");
}
}
public class test {
public static void main(String[] args) {
AA aa = new BB();
}
}
//运行结果:父类代码块
// 子类代码块

比如这里BB为AA 的子类, 两个类里均有代码块,当main方法里创建BB类对象时,会先输出父类代码块里的东西后输出子类代码块的东西

3.使用类的静态成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CC {
public static void main(String[] args) {
System.out.println(dd.sum);
}
}
class dd{
public static int sum=10;
static {
System.out.println("代码块");

}
}
//输出结果:代码块
// 10

值得注意的是,如果我们把上面代码块前面的static去掉,则不会输出代码块里面的内容。只是使用类的静态成员,代码块不会被执行

  • 创建一个对象时,在一个类中的调用顺序

1.调用静态代码块和静态属性初始化时,二者优先级相同,如果有多个静态代码块和多个静态属性初始化,则按照它们定义的顺序调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class A {
public static void main(String[] args) {
AA a=new AA();
}
}
class AA{
static {
System.out.println("代码块");
}
private static int n1=getN1();
public static int getN1(){
System.out.println("getN1被调用");
return 10;
}
}

上面是先调用静态代码块,再调用静态属性初始化,则输出结果为: 代码块

getN1被调用

如果将二者调换成如下顺序,那么输出结果将颠倒

1
2
3
4
private static int n1=getN1();
static {
System.out.println("代码块");
}

2.调用普通代码块和普通属性初始化时,同上,顺序依旧取决于定义的先后

3.当普通与静态混合使用时,静态代码块与属性初始化永远比普通代码块与属性要先执行,然后各自依照定义的顺序执行。此外, 构造器的优先级是最低的。现在我们补充一下上面的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AA{
public AA(){
System.out.println("构造器被调用");
}
private int n2=getN2();
{
System.out.println("普通代码块");
}
public int getN2(){
System.out.println("getN2被调用");
return 10;
}
static {
System.out.println("静态代码块");
}
private static int n1=getN1();
public static int getN1(){
System.out.println("getN1被调用");
return 10;
}
}

可以看到新增了普通代码块和普通属性初始化,还有构造器。但运行后发现顺序正好相反。先输出“静态代码块”和“getN1被调用”,然后再输出“普通代码块”和“getN2被调用”,最后才输出“构造器被调用”。上面这段代码很好的诠释了三者之间的顺序。

那么在继承关系中,它们的顺序又是什么呢?那么就引出我们即将要说的内容了

  • 创建一个子类对象时,在父类与子类中的调用顺序

还是老样子,先找静态,静态执行完之后再回去找普通。话不多说,上石山代码

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
public class test {
public static void main(String[] args) {
new B();
}
}
class A{
private static int n1=getVal01();
static{
System.out.println("A静态代码块");
}
{
System.out.println("A普通代码块");
}
public int n3=getVal02();
public static int getVal01(){
System.out.println("getVal01");
return 10;
}
public int getVal02(){
System.out.println("getVal02");
return 10;
}
public A(){
System.out.println("A构造器");
}
}
class B extends A{
private static int n3=getVal03();
static{
System.out.println("B静态代码块");
}
{
System.out.println("B普通代码块");
}
public static int getVal03(){
System.out.println("getVal03");
return 10;
}
public int getVal04(){
System.out.println("getVal04");
return 10;

}
public B(){
//super();
//注意一定不能忽略掉子类的suepr
System.out.println("B构造器");
}
}

B类继承A,当我们在main方法中创建B类对象以后,调用顺序如下

  1. 父类的静态代码块和静态属性初始化
  2. 子类的静态代码块和静态属性初始化
  3. 父类的普通代码块和普通属性初始化
  4. 父类的构造方法
  5. 子类的普通代码块和普通属性初始化
  6. 子类的构造方法

一句话总结一下,先找静态,再找普通,每一次寻找的顺序都是由父类往子类找

  • 类处于继承关系时的顺序

前面的知识中提到,创建一个类时,默认隐藏了super语句,如果父类中有代码块则先输出父类的代码块,这是容易忽略的一点,面试的高频考点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class test {
public static void main(String[] args) {
BB aa=new BB();
}
}
class AA{
{
System.out.println("AA代码块");
}
public AA(){
System.out.println("AA构造器");
}
}
class BB extends AA{
//super();
{
System.out.println("BB代码块");
}
public BB(){
System.out.println("BB构造器");
}
}

显而易见,输出结果为

1
2
3
4
AA代码块
AA构造器
BB代码块
BB构造器

使用场景

设计模式之【单例设计模式】

采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。下面我们来介绍两种简单的模式

  • 饿汉式

​ 1.构造器私有化

​ 2.类的内部创建对象

​ 3.向外暴露一个静态的公共方法

现在我们用代码实现该功能:一个人同时最多只能有一个女朋友

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class singleTest01 {
public static void main(String[] args) {
Girlfriend gf=Girlfriend.getInstance();
System.out.println(gf.toString());
}
}
class Girlfriend{
private String name;
private Girlfriend(String name){
this.name=name;
}
private static Girlfriend gf=new Girlfriend("若兰");
public static Girlfriend getInstance(){
return gf;
}
@Override
public String toString() {
return "name=" + name;
}
}

由于我们在构造器前面加上了private,在主方法new是行不通的,这也就避免了我们创建很多新对象。然后我们在此类里创建了一个新对象,但此时我们还不能在主方法里直接调用,需要用到另一个公共static方法:getInstance,返回该对象,这样我们就能在主方法里调用了。要注意这里的个体Instance方法的返回类型是类名。

但是,饿汉式还有个缺点,无论你有没有用到这个实例对象,饿汉式都会创建,这时候可能会造成资源浪费,下面我们的懒汉式就能很好解决这个问题了

  • 懒汉式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class singleTest01 {
public static void main(String[] args) {
System.out.println(Girlfriend.n);
}
}
class Girlfriend{
private String name;
public static int n=1;
private Girlfriend(String name){
this.name=name;
System.out.println("构造器被调用");
}
private static Girlfriend gf;//默认为空
public static Girlfriend getInstance(){
if(gf==null){
gf=new Girlfriend("若兰");
}
System.out.println("getInstance被调用");
return gf;
}
}

还是上面的案例,这里我们新创建了一个静态属性,众所周知当使用静态属性的时候默认加载这个类,但结果并没有“输出构造器被调用”以及“getInstance被调用“,这说明懒汉式并不自动创建实例对象,只有使用getInstance方法才会返回该对象,再次调用时,会返回上次创建的对象。不会造成资源的浪费

  • 区别

1.二者最主要的区别就是在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,而懒汉式实在使用时才创建

2.饿汉式不存在线程安全问题,懒汉式存在线程安全问题

final

使用情况

  • 不希望类被修饰,可以用final修饰
  • 不希望父类的某个方法被子类覆盖/重写,可以用final修饰
  • 不希望类的某个属性的值被修改,可以用final修饰
  • 当不希望某个局部变量被修改,可以用final修饰

细节

  • final修饰的属性又叫常量
  • final修饰的属性在定义时,必须赋初值,且不能再修改,赋值可以在如下位置之一

​ 1.定义时

​ 2.在构造器中

​ 3.在代码块中

  • 如果final修饰的属性是静态的,则初始化的位置只能是定义和静态代码块中,不能在构造器中赋值
  • final类不能被继承,但可以实例化对象
  • 如果类不是final类,但是含有final方法,此方法虽然不能重写,但可以继承
  • 一般来说如果一个类已经是final类了,不需要再将其方法用final修饰
  • final不能修饰构造器
  • final和static往往搭配使用,效率更高,不会导致类加载
1
2
3
4
5
6
7
8
9
10
11
public class test01 {
public static void main(String[] args) {
System.out.println(A.a);
}
}
class A{
static final int a=100;
static{
System.out.println("代码块");
}
}

一般来说当调用A类静态属性会加载整个类,但这里我们将属性用final修饰,可以做到仅仅调用这个属性而不加载类,从而提高效率

抽象类

所谓的抽象方法就是没有实现的方法,没有方法体。当一个类中存在抽象方法时,需要将该类声明为抽象类。一般来说,抽象类会被继承,由其子类来实现抽象方法

细节

  • 语法:abstract 方法名注意后面不能加花括号,没有方法体
  • 抽象类的价值更多是设计,设计者设计好后,让子类实现,在框架用的较多
  • 抽象类不能被实例化
  • 抽象类可以有任意成员,比如非抽象方法、构造器、静态属性等等,不一定要包含abstract方法
1
2
3
4
5
6
7
8
9
10
abstract class A{
private int a;
public A(){
this.a=a;
System.out.println("这是构造器");
}
public void B(){
System.out.println("...");
}
}
  • abstract只能修饰类和方法,不能修饰属性或其他的
  • 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,否则将自己声明为abstract类
  • 抽象方法不能使用private、final和static来修饰,因为这些关键字都是和重写相违背的

实践-设计模式之【模板设计模式】

假设现在我们要实现这么一个方法,有很多类,每个类包含几个方法,统计这些方法运行的时间

基础版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class A {
public void job(){
long sum=0;
long start=System.currentTimeMillis();//统计当前时间
for(int i=1;i<=10000000;i++){
sum+=i;
}
long end=System.currentTimeMillis();
System.out.println("运行时间为:"+(end-start));
//前后时间一减就是运行时间
}
}
public class test {
public static void main(String[] args) {
A a=new A();
a.job();
}
}

这里我们就写了一个类,其他的类同理。但我们会发现一个问题,倘若要写很多类,每一个类的方法中我们都要写一个统计时间的代码,那将很耗费时间。能不能改进呢?当然,就用到了所谓的“模板设计模式”

终极版

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 abstract class tamplate {
public abstract void job();//抽象方法
public void time(){//非抽象方法
long sum=0;
long start=System.currentTimeMillis();
job();
long end=System.currentTimeMillis();
System.out.println("运行时间为:"+(end-start));
}
}
public class A extends tamplate{
public void job(){
long sum=0;
for(int i=1;i<=10000000;i++){
sum+=i;
}
}
}
public class test {
public static void main(String[] args) {
A a=new A();
a.time();
}
}

这里我们将计算时间集成为一个方法,放在抽象类tamplate中,此外还有一个抽象方法job。当我们创建一个新类并继承tamplate时,需要对job方法重写,这样我们就不需要在每个方法体中编写计算时间的代码了,大大降低了繁琐程度

注意,这里运用到了动态绑定机制,当我们在主方法中调用time时,会先从子类中找有没有这个方法,然后在父类中寻找,当运行到time方法中的job时,会与运行类型绑定,自然就跳转到该运行类型的job方法中去了

接口

介绍

接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface itf{
public int a=100;
public void A();
default public void B(){
System.out.println("实现方法");
}//可以使用默认方法,需要使用default关键字修饰
public static void C(){
System.out.println("实现方法");
}//或者使用静态方法,用static修饰
}
class AA implements itf{
public void A(){
System.out.println("实现方法");
}
}

注意:在jdk7以前,接口里的所有方法都没有方法体,即都是抽象方法;而在jdk8以后接口类可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现

枚举和注解

枚举类

按照传统思路去写只拥有几个固定属性的对象(如季节等)是不适合的,因为能随意修改。我们引入了枚举类。

枚举是一组常量的集合,是一种特殊的类,里面只包含一组有限的特定的对象。

实现方式

  • 自定义类实现枚举

    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
    public class EnumerationClass01 {
    public static void main(String[] args) {
    System.out.println(season.SPRING.getName()+" "+season.SPRING.getDesc());
    System.out.println(season.SUMMER.getName()+" "+season.SUMMER.getDesc());
    System.out.println(season.AUTUMN.getName()+" "+season.AUTUMN.getDesc());
    System.out.println(season.WINTER.getName()+" "+season.WINTER.getDesc());
    }
    }
    class season{
    //构造器私有化,防止在外部直接新建(new)
    //去掉set(只读)防止属性被修改
    private String name;
    private String desc;
    //优化,加入final修饰
    public final static season SPRING =new season("春天","温暖");
    public final static season SUMMER =new season("夏天","炎热");
    public final static season AUTUMN =new season("秋天","凉爽");
    public final static season WINTER =new season("冬天","寒冷");
    private season(String name,String desc){
    this.desc=desc;
    this.name=name;
    }
    public String getName(){
    return name;
    }
    public String getDesc(){
    return desc;
    }
    }

    为了防止随意修改属性,我们将原本的setXXX方法删去,只提供getXXX方法,即只读。通常对枚举对象/属性使用final+static修饰(对外暴露对象),以实现底层优化。枚举对象名通常全部使用大写,常量的命名规范。

  • enum关键字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    enum season2{
    SPRING("春天","温暖"),
    SUMMER("夏天","炎热"),
    AUTUMN("秋天","凉爽 "),
    WINTER("冬天","寒冷");//写在前面,多个对象要用用逗号隔开
    private String name;
    private String desc;
    private season2(String name,String desc){
    this.desc=desc;
    this.name=name;
    }
    public String getName(){
    return name;
    }
    public String getDesc(){
    return desc;
    }
    }

    基本语法与上面略有不同,不过本质上都是类似的。值得注意的是,在这里 如果有多个枚举对象,需要用逗号隔开。还有就是枚举对象要写在最前面。

    当我们通过javap反编译class文件时,会发现我们用enum写的枚举类默认继承Enum类,而且是一个final类。

![屏幕截图 2024-06-04 200518](C:\Users\ASUS\Desktop\枚举和注解\屏幕截图 2024-06-04 200518-1717502927844-2.png)

​ 如果使用无参构造器创建枚举对象,则实参列表和小括号都可以省略

1
2
3
4
DAY();//DAY;
private String name;
private String desc;
private season2(){}

enum常用方法

这里我们还是用上面season2这个枚举类。

  • name()

    输出枚举对象的名字。

    1
    2
    season2 spring=season2.SPRING;//SPRING
    System.out.println(spring.name());
  • ordinal()

    输出该枚举对象的次序,第一个为0,第二个为1,以此类推……

    1
    System.out.println(spring.ordinal());//0

    由于我们的spring是第一个枚举对象,因此输出0.同理如果后面跟着summer,则输出1.

  • values()

    返回含有所有定义的枚举对象的数组。

    1
    2
    3
    4
    season2 v[]=spring.values();
    for(int i=0;i<4;i++){
    System.out.println(v[i]);
    }

    这里的v就包含了四个季节。

  • valueOf()

    根据输入的字符串再所有枚举对象中查找,如果找到了就返回,没有找到就报错。

    1
    2
    season2 spring2=season2.valueOf("SPRING");
    System.out.println(spring==spring2);//true

    注意这里的pring2和上文提到的spring是一个东西,当我们判断它们是否相等时,输出true,可以验证。如果我们将后面的”SPRING“改为”SPING“那么系统找不到,就报错。

  • compareTo()

    把两个对象的序号比较,返回的结果为前面的序号与后面的序号之差。

    1
    System.out.println(season2.SPRING.compareTo(season2.AUTUMN));

    SPRING的序号为0,AUTUMN的序号为2,因此输出结果为-2.

enum实现接口

  • 使用enum关键字就不能继承其他的类了,因为enum隐式继承了Enum,而Java是单继承机制

  • 枚举类实现接口如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class EnumExercise02 {
    public static void main(String[] args) {
    A.MEIJU.m();
    }
    }
    enum A implements AA{
    MEIJU;
    public void m() {
    System.out.println("m");
    }
    }
    interface AA{
    public void m();
    }

    注解

    注解也被称为元数据,用于修饰解释包、类、方法、属性、构造器和局部变量等数据信息。

    基本介绍

    使用Annotation时要在其前面增加@符号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。

    下面介绍三个基本的Annotation:

    • @Override

      限定某个方法,重写父类方法,该注解只能用于方法。如果写了@Override注解,编译器就会去检查该方法是否真的重写了父类的方法,如果重写了,编译通过,否则编译错误。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class A{
      public void m1(){};
      }
      class B extends A{
      @java.lang.Override
      public void m1(){
      System.out.println("m1");
      }
      }

      @Override只能修饰方法,不能修饰其他类、包、属性等。

    • @Deprecated

      用于表示某个程序元素(类,方法等)已经过时。可以修饰方法、包、类、字段、参数等等。

      1
      2
      3
      4
      5
      6
      7
      8
      @java.lang.Deprecated
      class A1{//过时不是不能用,只是不推荐使用
      @java.lang.Deprecated
      public int n1=1;
      @java.lang.Deprecated
      public void m(){
      }
      }

      可以做版本升级过渡使用。

    • @SuppressWarnings

      当我们不希望看到警告的时候,可以用@SuppressWarnings来抑制警告信息。在{“ “}中,可以写入你希望抑制的警告信息。

    1
    @SuppressWarnings("all")

    @SuppressWarnings抑制的范围和你放置的位置相关。

元注解

元注解为修饰注解的注解。

元注解主要分为

  • @Retention

    只能修饰注解定义,用于指定该注解可以保留多长时间。

  • @Target

    用于修饰注解定义,用于指定被修饰的注解能用于哪些程序元素。

  • @Document

    用于指定被改元注解修饰的注解类将被javadoc工具提取成文档,在文档中显示。

  • @Inherited

    被他修饰的注解将具有继承性。如果某个类使用了被@Inherited修饰的注解,则其子类将自动具有该注解。

异常

基本概念

​ Java语言中,将程序执行中发生的不正常情况称为“异常”,但开发过程中的语法错误和逻辑错误不是异常。

分类

  • Error:Java虚拟机无法解决的严重问题。比如系统内部错误,资源耗尽(栈溢出,内存不足等等),Error是严重错误,程序会崩溃

  • Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。分为两大类:

    • 运行时异常(程序运行时,发生的异常)程序员应该避免其出现这样的异常,编译器检查不出来,这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
    • 编译时异常(编程时,编译器检查出的异常)编译器要求必须处置的异常。

常见的运行时异常

  • NullPointException空指针异常:

    ​ 当应用程序试图在需要对象的地方使用null时,抛出该异常。

    1
    2
    3
    4
    5
    6
    public class NullPointException {
    public static void main(String[] args) {
    String name=null;
    System.out.println(name.length());
    }
    }

    ​ 这里我们输出了一个空字符串的长度,显然是错误的,就会抛出Exception in thread "main" java.lang.NullPointerException的异常。

  • ArithmeticException数学运算异常:

    ​ 当出现异常的运算条件时,比如一个数除以0等等,就会抛出该异常。

  • ArrayIndexOutOfBoundsException数组越界异常:

    1
    2
    3
    4
    5
    6
    public class ArrayIndexOutOfBoundsException {
    public static void main(String[] args) {
    int[]a=new int[4];
    System.out.println(a[6]);
    }
    }

    抛出Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException

  • ClassCastException类型转换异常:

    ​ 当试图将对象强制转换为一个不是实例的子类时,抛出该异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class ClassCastException {
    public static void main(String[] args) {
    A b=new B();
    B b1=(B)b;
    C b2=(C)b;
    }
    }
    class A{}
    class B extends A{}
    class C extends A{}

    ​ 主方法中第二条语句的向下转型是对的,因为b的运行类型本来就是B.而第三条语句中的向下转型明显就是两个无关的类(B和C)故抛出异常。

  • NumberFormatException数字格式不正确异常

    ​ 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。

    1
    2
    3
    4
    5
    6
    public class NumberFormatException {
    public static void main(String[] args) {
    String name="aaa";
    int num=Integer.parseInt(name);
    }
    }

    这里我们不能将一个字母类型的字符串转换成整数类型,因此抛出异常。

异常处理

异常处理的方式

  • try-catch-finally

    ​ 如果认为一段代码有异常抛出,可以用以下代码捕获异常。系统将异常封装成Exception对象e,传递给catch.

    1
    2
    3
    4
    5
    6
    7
    8
    try{
    ...
    }catch(Exception e){
    //捕获到异常
    }finally{
    //不管try代码块是否有异常发生,始终要执行finally
    //主要用来资源的关闭
    }

    ​ 如果没有发生异常,catch代码块不执行,但是finally仍然执行。如果没有finally也可以。

    ​ 如果发生了异常,则异常后面的代码不会执行,直接进入到catch块,如果有finally,最后还需要执行finally里面的语句。比如下面的例子,在Integer.parseInt(name)处遇到了异常,那么我们就直接进入catch,输出异常,而不会执行输出name的语句。

    1
    2
    3
    4
    5
    6
    7
    try {
    String name="aaa";
    int num=Integer.parseInt(name);
    System.out.println(name);
    } catch (NumberFormatException e) {
    System.out.println(e);
    }
  • throws

    如果一个方法中的语句执行时可能生成某种异常,但不能确定怎么处理这种异常,则应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。

    1
    2
    3
    public void f1() throws FileNotFoundException {//文件不存在异常
    FileInputStream fis=new FileInputStream("...");
    }//让调用f1方法的调用者处理异常

    有以下两点值得注意

    • throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类;
    • throws关键字后面也可以是异常列表,即可以抛出多个异常。

    遇到异常可以抛给上一级,或者使用try-catch-finally机制。值得注意的是,try-catch-finally机制和throws机制只能二选一,不能两个都用。直到遇到JVM(最顶级)它处理异常的机制为输出异常信息,退出程序。

    如果一段代码既没有用try-catch-finally机制也没有用throws机制,那么系统默认使用throws.

使用细节

  • 对于编译异常,程序中必须处理,比如try-catch或throws

    1
    2
    3
    4
    5
    6
    7
    8
    class AA{
    public static void m1() throws FileNotFoundException{
    FileInputStream fis=new FileInputStream("...");
    }
    public void m2(){//这么写报错,必须得在后面也加上throws FileNotFoundException或try-catch
    m1();
    }
    }

    m1抛出了一个编译异常,当m2调用时,m2不能不处理从m1接受的编译异常,因此需要使用try-catch或throws.

  • 对于运行时异常,程序中如果没有处理,默认就是throws方式处理

    1
    2
    3
    4
    5
    6
    7
    class AA{
    public static void m1() throws ArithmeticException{
    }
    public void m2(){
    m1();
    }
    }

    依旧是上面的例子,在这里ArithmeticException是运行时异常,程序默认以throws方式处理,故不会报错

  • 子类重写父类的方法时,对抛出异常的规定:子类重写的方法,所抛出异常类型要么和父类抛出的异常一致,要么为父类抛出异常的子类型

    1
    2
    3
    4
    5
    6
    7
    8
    class Father{
    public void m1()throws RuntimeException{
    }
    }
    class Son extends Father{
    public void m1()throws ArithmeticException{
    }
    }

​ 上面的RuntimeExceptionArithmeticException的父类,因此可以重写

  • 在throws过程中,如果有方法try-catch,就相当于处理异常,就不需要使用throws了

自定义异常

当程序中出现了某些错误时,但该错误信息并没有在Throwable中表述处理,这个时候可以自己设计异常累,用于表示该错误信息。

我们直接看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class test {
public static void main(String[] args) {
int age=1;
if(age<18){
throw new AgeException("年龄需要大于18岁");
}
System.out.println("年龄正确");
}
}
class AgeException extends RuntimeException{
public AgeException(String age){
super(age);
}
}

我们要实现一个判断年龄的功能,如果年龄小于十八岁,那么抛出一个异常。这里我们写了一个类AgeException,它是继承了RuntimeException的一个类。我们在自定义异常的时候,如果继承Exception,属于编译异常;如果继承RuntimeException,属于运行异常。当需要抛出异常的时候,构造器将字符串传给父类,以此实现相关功能。

一般情况下,我们自定义异常是继承RuntimeException,把自定义异常写成运行时异常的好处是可以使用默认的处理机制。

throws和throw的区别

意义 位置 后面跟的东西
throws 异常处理的一种方式 方法声明处 异常类型
throw 手动生成异常对象的关键字 方法体中 异常对象

常用类

包装类

分类

针对八种基本数据类型相应的引用类型

数据类型 包装类
boolean Boolean
char Character
byte Byte
short Short
int Integer
long Long
float Float
double Double

装箱和拆箱

首先先解释一下什么是装箱和拆箱。装箱就是基本类型转换为包装类型,而拆箱就是包装类型转换为基本类型。

  • jdk5以前是手动装箱和拆箱

    1
    2
    3
    4
    5
    6
    7
    int i=100;
    //手动装箱第一种写法
    Integer integer=new Integer(i);
    //第二种写法
    Integer integer1=Integer.valueOf(i);
    //手动拆箱
    int i1=integer.intValue();
  • jdk5以后是自动装箱和拆箱

    1
    2
    3
    4
    //自动装箱
    Integer integer2=i;
    //自动拆箱
    int i2=integer2;

其实,自动装箱底层仍然使用的是手动装箱的方法,系统自动帮你写好了。

其他包装类的用法类似,这里就不过多赘述了。

小练习

1
2
Object obj=true?new Integer(1):new Double(2.0);
System.out.println(obj);

分析如上语句的输出结果。

三目运算符,前面为真,返回冒号前面的值,因此Object obj=new Integer(1),按理说应该输出1,但是这里的三目运算符要当成一个整体,因此要输出1.0

包装类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Integer和String互相转换
//包装类-->String类型
Integer i=10;
//方式1
String s1=i.toString();
//方式2
String s2=String.valueOf(i);
//方式3
String s3=i+"";
//String类型-->包装类
String s4="12";
//方式1
Integer j=new Integer(s4);
//方式2
Integer j2=Integer.valueOf(s4);

Integer类

我们来看几个样例

1
2
3
Integer i1=new Integer(1);
Integer i2=new Integer(1);
System.out.println(i1==i2);

显而易见,即便是两个值均为1,但他们是新创建的两个对象,因此不等。

1
2
3
Integer j1=1;
Integer j2=1;
System.out.println(j1==j2);

这里很容易就想到j1=j2,但真的是这样吗?上面我们提到了自动装箱的实质:Integer.valueOf.我们来看一下这个方法的源码:

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);
}

可以看到这里有一个限制条件,传入的参数必须在一个范围之内,否则会创建一个对象。因此这里判断j1j2是否相等就看传入参数的范围。IntegerCache.lowIntegerCache.high到底表示什么呢?我们再看它的源码可知,最小值为-127,最大值为128.因此,如果传入的参数小于等于128且大于等于-127,都不会创建新的对象。故这里j1j2相等。

1
2
3
Integer k1=128;
Integer k2=128;
System.out.println(k1==k2);

所以这一段代码就不相等了。

1
2
3
Integer i1=128;
int i2=128;
System.out.println(i1==i2);

只要有基本数据类型,直接判断值是否相等,因此上面输出true.

String类

概述

  • 字符串的字符使用Unicode字符编码,一个字符无论是字母还是汉字都占两个字节。

  • String类实现了接口Serializable(String可以串行化:可以在网络传输)、接口Comparable(String对象可以比较大小)

  • String是final类,不能被其他类继承

  • 我们在翻String的源码时,会有这样一句话private final char value[];

    表示用于存放字符串内容,此外这个value被final修饰,故不可以修改。这里的修改不是指字符串的内容不能修改,而是value不能指向新的地址。

    1
    2
    3
    4
    final char v[] ={'A','B'};
    v[0]='a';
    char v2[]={'C','D'};
    v=v2;//error

    v字符数组我们用final修饰,我们将他指向v2直接报错。

创建

  • 直接赋值String s="xxx";

    先从常量池查看是否有"xxx"数据空间,如果有,直接指向;如果没有,则重新创建,然后指向。s最终指向的是常量池的空间地址。

  • 调用构造器String s=new String("xxx");

    先在堆中创建空间,里面维护了value的属性,指向常量池的"xxx"空间。如果常量池没有"xxx",重新创建,如果有,直接通过value指向,最终指向的是堆中的空间地址。

内存

常用方法

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
55
56
57
58
public static void main(String[] args) {

//1.equals
String str1="hello";
String str2="Hello";
String str3="Hellohello";
String str4="helloHellohello";
System.out.println(str1.equals(str2));//判断内容是否相等,区分大小写

//2.equalsIgnoreCase
if(str1.equalsIgnoreCase(str2)){//判断内容是否相等,忽略大小写
System.out.println("yes");
}
else System.out.println("no");

//3.indexOf
System.out.println(str3.indexOf(str1));//获取字符在字符串第一次出现的索引,从0开始,找不到返回-1

//4.lastIndexOf
System.out.println(str4.lastIndexOf(str1));//获取字符在字符串最后一次出现的索引

//5.substring
System.out.println(str3.substring(5));//从给定索引开始截取后面所有的内容
System.out.println(str3.substring(0,5));//从0开始截取,截取到索引为5的字符串,不包括5

//6.toUpperCase
System.out.println(str1.toUpperCase());//将字符串转换成大写

//7.toLowerCase
System.out.println(str1.toUpperCase().toLowerCase());//将字符串转换成小写

//8.concat
System.out.println(str1.concat(str2));//拼接

//9.replace
str3=str3.replace("hello","world");//将原来字符串里的hello替换成world
System.out.println(str3);

//10.split
String poem="锄禾日当午,汗滴禾下土,谁知盘中餐,粒粒皆辛苦";
String[]split=poem.split(",");//以逗号分割字符串,并保存到字符串数组里
for(int i=0;i<split.length;i++){
System.out.println(split[i]);
}

//11.toCharArray
char[]c=str1.toCharArray();//将String转换为字符数组
for(int i=0;i<c.length;i++){
System.out.println(c[i]);
}

//12.format
String name="jack";
int age=18;
String info=String.format("我的名字是%s,年龄是%d",name,age);
System.out.println(info);
//%s,%d等叫做占位符,占位符有后面的变量替换
}

StringBuffer

结构

StringBuffer代表可变的字符序列,可以对字符串内容进行增删,很多方法与String是相同的,但StringBuffer是可变长度的。

  • StringBuffer的直接父类是AbstractStringBuffer
  • StringBuffer实现了Serializable,即它的对象可以串行化
  • AbstractStringBuffer中,有属性char[ ]value,但它不是final修饰的。该value数组存放字符串内容,存放在堆中
  • StringBuffer是一个final类,不能被继承

String和StringBuffer的对比

  • String保存的是字符串常量,里面的值不能更改,每次String类的更新实际上就是更改地址,效率较低。
  • StringBuffer保存的是字符串变量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容,不用每次更新地址,效率较高。

构造

1、构造一个不带字符的字符串缓冲区,其初始容量为16个字符。我们进到StringBuffer的源码可以看到它的构造器:

1
StringBuffer stringBuffer=new StringBuffer();
1
2
3
public StringBuffer() {
super(16);
}

故容量(capacity)为16.

2、构造一个不带字符,但具有指定初始容量的字符串缓冲区,即对char[]的大小进行指定。同样我们可以看它的源码:

1
StringBuffer stringBuffer1=new StringBuffer(10);
1
2
3
public StringBuffer(int capacity) {
super(capacity);
}

3、构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容

1
StringBuffer stringBuffer2=new StringBuffer("tom");
1
2
3
public StringBuffer(String str) {
super(str);
}

转换

  • String–>StringBuffer

    方式1:使用构造器

    1
    2
    String s="hello";
    StringBuffer b1=new StringBuffer(s);

    方式2:使用append方法

    1
    2
    StringBuffer b1=new StringBuffer();
    b1.append(s);
  • StringBuffer–>String

    方式1:使用toString方法

    1
    String s2=b1.toString();

    方式2:使用构造器

    1
    String s3=new String(b1);

方法

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

//追加
StringBuffer s=new StringBuffer("hello");
s.append("a");
s.append("1").append("2").append("3");
System.out.println(s);

//删除
s.delete(4,5);//删除大于4但小于等于5的字符
System.out.println(s);

//替换
s.replace(4,5,"%");//将大于4但小于等于5的字符替换为指定字符
System.out.println(s);

//查找
int index= s.indexOf("123");
System.out.println(index);

//插入
s.insert(5,"***");
System.out.println(s);
}

StringBuilder

StringBuilderStringBuffer均代表可变的字符序列,方法是一样的。只不过StringBuilder用于单线程,StringBuffer用于多线程。

  • StringBuilder实现了Serializable,即它的对象可以串行化

  • StringBuilder是一个final类,不能被继承

  • StringBuilder对象字符序列仍然是存放在其父类AbstractStringBuffer,的char[]value中,因此也存放在堆中

  • StringBuilder所有方法没有做互斥的处理,因此在单线程的情况下使用

String、StringBuffer和StringBuilder的比较

  • String:不可变字符序列,效率低,但复用率高
  • StringBuffer:可变字符序列,效率高,线程安全
  • StringBuilder:可变字符序列,效率最高,线程不安全

如果字符串存在大量的修改操作,并且在单线程的情况下,使用StringBuilder;如果在多线程的情况下,使用StringBuffer;如果字符串很少修改,被多个对象引用,使用String.

Math类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//abs 绝对值
int abs=Math.abs(-1);
//pow 求幂
double pow=Math.pow(2,4);
//ceil 向上取整
double ceil=Math.ceil(3.0001);
//floor 向下取整
double floor=Math.floor(3.9999);
//round 四舍五入
long round=Math.round(5.55);
//sqrt 开方
double sqrt=Math.sqrt(4);
//random 求随机数
//返回大于等于0 但 小于1 之间的一个随机小数
for(int i=0;i<10;i++){
System.out.println(Math.random());
}

Arrays类

Arrays里面包含了一系列静态方法,用于管理或操作数组。下面具体介绍几种方法。

下面所有代码一定不能忘记导入java.util.Arrays

  • toString

    1
    2
    Integer[] a ={1,2,3,4,5};
    System.out.println(Arrays.toString(a));

    遍历数组

  • sort

    1
    Arrays.sort(a);

    sort排序后,会直接影响到实参。

    此外也可以通过一个接口Comparator实现定制排序

    1
    2
    3
    4
    5
    6
    7
    8
    Arrays.sort(a, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
    return o1-o2;
    //源码最终执行到binarysort
    //o1-o2还是o2-o1直接决定的返回的正负性,从而决定了排序顺序,即从大到小还是从小到大
    }//实现Comparator接口的匿名内部类
    });

    根据指定比较器产生的顺序对指定的对象数组进行排序,这里的Comparator就是比较器。下面来看一个具体的例子:

    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 bubble_sort {
    public static void main(String[] args) {
    Integer a[]={5,2,1,4,3};
    bubbleSort(a, new Comparator() {
    public int compare(Object o1, Object o2) {
    int i1=(Integer)o1;
    int i2=(Integer)o2;
    return i2-i1;
    }
    });//小括号不能漏
    System.out.println(Arrays.toString(a));
    }
    public static void bubbleSort(Integer a[],Comparator c){
    int i,j;
    for(i=0;i<a.length;i++){
    for(j=i+1;j<a.length;j++){
    if(c.compare(a[i],a[j])>0){
    int temp=a[j];
    a[j]=a[i];
    a[i]=temp;
    }
    }
    }
    }
    }

    首先看到bubblesort里面的结构非常熟悉,这就是我们之前提到的匿名内部类,尤以结尾的小括号瞩目。而冒泡排序的关键在于两趟循环指向值的大小比较,因此我们在这里“设限”,将比较器塞进去,即compare方法,这样我们才能根据上面指定的正负性来合理调用if判断。

  • binarySearch