我的个人博客

哪有什么胜利可言,挺着就代表着一切


  • 首页

  • 归档

菜鸟话Java---Comparable接口和Comparator接口

发表于 2019-03-25 | 分类于 java

转自毛先森的博客

Java中实现对象的比较:Comparable接口和Comparator接口

在实际应用中,我们往往有需要比较两个自定义对象大小的地方。而这些自定义对象的比较,就不像简单的整型数据那么简单,它们往往包含有许多的属性,我们一般都是根据这些属性对自定义对象进行比较的。所以Java中要比较对象的大小或者要对对象的集合进行排序,需要通过比较这些对象的某些属性的大小来确定它们之间的大小关系。

一般,Java中通过接口实现两个对象的比较,比较常用就是Comparable接口和Comparator接口。首先类要实现接口,并且使用泛型规定要进行比较的对象所属的类,然后类实现了接口后,还需要实现接口定义的比较方法(compareTo方法或者compare方法),在这些方法中传入需要比较大小的另一个对象,通过选定的成员变量与之比较,如果大于则返回1,小于返回-1,相等返回0。

Comparable接口

什么是Comparable接口

此接口强行对实现它的每个类的对象进行整体排序。此排序被称为该类的自然排序 ,类的 compareTo方法被称为它的自然比较方法 。实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort )进行自动排序。实现此接口的对象可以用作有序映射表中的键或有序集合中的元素,无需指定比较器。

实现什么方法

1
2
3
4
5
6
int compareTo(T o)
比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

参数: o - 要比较的对象。
返回:负整数、零或正整数,根据此对象是小于、等于还是大于指定对象。
抛出:ClassCastException - 如果指定对象的类型不允许它与此对象进行比较。

实例

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package com.mxl.algorithlm;

import java.util.Date;
/**
* 因为要实现对ConsumInfo对象的排序,所以在ConsunInfo类中要实现Comparable接口,也就是要实现compareTo()方法
* 具体的比较参照:依次按照price、uid进行倒序排序
* @author breeze
*
*/
public class ConsumInfo implements Comparable<ConsumInfo> {
private int uid;
private String name;
private double price;
private Date datetime;

public ConsumInfo() {
// TODO Auto-generated constructor stub
}

public ConsumInfo(int uid,String name,double price,Date datetime){
this.uid = uid;
this.name = name;
this.price = price;
this.datetime = datetime;

}

public int getUid() {
return uid;
}

public void setUid(int uid) {
this.uid = uid;
}

public String getName() {
return name;
}

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

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public Date getDatetime() {
return datetime;
}

public void setDatetime(Date datetime) {
this.datetime = datetime;
}


@Override
public String toString() {
return "ConsumInfo [uid=" + uid + ", name=" + name + ", price=" + price
+ ", datetime=" + datetime + "]";
}
/**
* 这里比较的是什么, Collections.sort方法实现的就是按照此比较的东西排列
* 顺序(从小到大):
* if(price < o.price){
return -1;
}
if(price > o.price){
return 1;
}
* 倒序(从大到小):
* if(price < o.price){
return 1;
}
if(price > o.price){
return -1;
}
*
*/
@Override
public int compareTo(ConsumInfo o) {
//首先比较price,如果price相同,则比较uid
if(price < o.price){
return -1;
}
if(price > o.price){
return 1;
}

if(price == o.price){
if(uid < o.uid){
return -1;
}
if(uid > o.uid){
return 1;
}
}
return 0;
}


}

//测试类

package com.mxl.algorithlm;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

public class ConsumInfoTest {

public static void main(String[] args) {

ConsumInfo consumInfo1 = new ConsumInfo(100, "consumInfo1", 400.0,new Date());
ConsumInfo consumInfo2 = new ConsumInfo(200, "consumInfo1", 200.0,new Date());
ConsumInfo consumInfo3 = new ConsumInfo(300, "consumInfo1", 100.0,new Date());
ConsumInfo consumInfo4 = new ConsumInfo(400, "consumInfo1", 700.0,new Date());
ConsumInfo consumInfo5 = new ConsumInfo(500, "consumInfo1", 800.0,new Date());
ConsumInfo consumInfo6 = new ConsumInfo(600, "consumInfo1", 300.0,new Date());
ConsumInfo consumInfo7 = new ConsumInfo(700, "consumInfo1", 900.0,new Date());
ConsumInfo consumInfo8 = new ConsumInfo(800, "consumInfo1", 400.0,new Date());

List<ConsumInfo> list = new ArrayList<ConsumInfo>();
list.add(consumInfo1);
list.add(consumInfo2);
list.add(consumInfo3);
list.add(consumInfo4);
list.add(consumInfo5);
list.add(consumInfo6);
list.add(consumInfo7);
list.add(consumInfo8);
System.out.println("排序前:");
//排序前
for(ConsumInfo consumInfo : list ){
System.out.println(consumInfo);
}

Collections.sort(list);//排序
System.out.println("排序后:");
//排序后
for(ConsumInfo consumInfo :list){
System.out.println(consumInfo);
}
}
}

Comparator接口

与上面的Comparable接口不同的是:

  • Comparator位于包java.util下,而Comparable位于包java.lang下。
  • Comparable接口将比较代码嵌入需要进行比较的类的自身代码中,而Comparator接口在一个独立的类中实现比较。
  • 如果前期类的设计没有考虑到类的Compare问题而没有实现Comparable接口,后期可以通过Comparator接口来实现比较算法进行排序,并且为了使用不同的排序标准做准备,比如:升序、降序。
  • Comparable接口强制进行自然排序,而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
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package test;

import java.util.Comparator;
/**
* 具体的比较类(比较器),实现Comparator接口
* @author breeze
*
*/
public class ComparatorConsunInfo implements Comparator<ConsumInfo> {
/**
* 顺序(从小到大):
* if(price < o.price){
return -1;
}
if(price > o.price){
return 1;
}
* 倒序(从大到小):
* if(price < o.price){
return 1;
}
if(price > o.price){
return -1;
}
*/
@Override
public int compare(ConsumInfo o1, ConsumInfo o2) {
//首先比较price,如果price相同,则比较uid
if(o1.getPrice() > o2.getPrice()){
return 1;
}

if(o1.getPrice() < o2.getPrice()){
return -1;
}

if(o1.getPrice() == o2.getPrice()){
if(o1.getUid() > o2.getUid()){
return 1;
}
if(o1.getUid() < o2.getUid()){
return -1;
}
}
return 0;
}

}


/**
* 需要进行比较的类
* @author breeze
*
*/
public class ConsumInfo{
private int uid;
private String name;
private double price;
private Date datetime;

public ConsumInfo() {
// TODO Auto-generated constructor stub
}

public ConsumInfo(int uid,String name,double price,Date datetime){
this.uid = uid;
this.name = name;
this.price = price;
this.datetime = datetime;

}

public int getUid() {
return uid;
}

public void setUid(int uid) {
this.uid = uid;
}

public String getName() {
return name;
}

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

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

public Date getDatetime() {
return datetime;
}

public void setDatetime(Date datetime) {
this.datetime = datetime;
}

@Override
public String toString() {
return "ConsumInfo [uid=" + uid + ", name=" + name + ", price=" + price
+ ", datetime=" + datetime + "]";
}

}


//测试类
public class ConsumInfoTest {

public static void main(String[] args) {

ConsumInfo consumInfo1 = new ConsumInfo(100, "consumInfo1", 400.0,new Date());
ConsumInfo consumInfo2 = new ConsumInfo(200, "consumInfo1", 200.0,new Date());
ConsumInfo consumInfo3 = new ConsumInfo(300, "consumInfo1", 100.0,new Date());
ConsumInfo consumInfo4 = new ConsumInfo(400, "consumInfo1", 700.0,new Date());
ConsumInfo consumInfo5 = new ConsumInfo(500, "consumInfo1", 800.0,new Date());
ConsumInfo consumInfo6 = new ConsumInfo(600, "consumInfo1", 300.0,new Date());
ConsumInfo consumInfo7 = new ConsumInfo(700, "consumInfo1", 900.0,new Date());
ConsumInfo consumInfo8 = new ConsumInfo(800, "consumInfo1", 400.0,new Date());

List<ConsumInfo> list = new ArrayList<ConsumInfo>();
list.add(consumInfo1);
list.add(consumInfo2);
list.add(consumInfo3);
list.add(consumInfo4);
list.add(consumInfo5);
list.add(consumInfo6);
list.add(consumInfo7);
list.add(consumInfo8);

System.out.println("排序前:");
//排序前
for(ConsumInfo consumInfo : list ){
System.out.println(consumInfo);
}
ComparatorConsunInfo comparatorConsunInfo = new ComparatorConsunInfo();//比较器
Collections.sort(list,comparatorConsunInfo);//排序
System.out.println("排序后:");
//排序后
for(ConsumInfo consumInfo :list){
System.out.println(consumInfo);
}
}
}

菜鸟话Java---Java内部类

发表于 2019-03-24 | 分类于 java

转载海子博客

Java内部类详解

  说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉。原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,
并且即使用到也很少去总结内部类的用法。今天我们就来一探究竟。下面是本文的目录大纲:

一. 内部类基础

二. 深入理解内部类

三. 内部类的使用场景和好处

四. 常见的与内部类相关的笔试面试题

若有不正之处,请多谅解并欢迎批评指正。

请尊重作者劳动成果,转载请标明原文链接:

http://www.cnblogs.com/dolphin0520/p/3811445.html

内部类基础

  在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。

成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Circle {
double radius = 0;

public Circle(double radius) {
this.radius = radius;
}

class Draw { //内部类
public void drawSahpe() {
System.out.println("drawshape");
}
}
}

这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle {
private double radius = 0;
public static int count =1;
public Circle(double radius) {
this.radius = radius;
}

class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
System.out.println(count); //外部类的静态成员
}
}
}

不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:

1
2
外部类.this.成员变量
外部类.this.成员方法

  虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Circle {
private double radius = 0;

public Circle(double radius) {
this.radius = radius;
getDrawInstance().drawSahpe(); //必须先创建成员内部类的对象,再进行访问
}

private Draw getDrawInstance() {
return new Draw();
}

class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
}
}
}

成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

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 Test {
public static void main(String[] args) {
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建

//第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
}
}

class Outter {
private Inner inner = null;
public Outter() {

}

public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}

class Inner {
public Inner() {

}
}
}

内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

局部内部类

  局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class People{
public People() {

}
}

class Man{
public Man(){

}

public People getWoman(){
class Woman extends People{ //局部内部类
int age =0;
}
return new Woman();
}
}

注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

匿名内部类

匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段Android事件监听代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scan_bt.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub

}
});

history_bt.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub

}
});

这段代码为两个按钮设置监听器,这里面就使用了匿名内部类。这段代码中的:

1
2
3
4
5
6
7
8
new OnClickListener() {

@Override
public void onClick(View v) {
// TODO Auto-generated method stub

}
}

就是匿名内部类的使用。代码中需要给按钮设置监听器对象,使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。当然像下面这种写法也是可以的,跟上面使用匿名内部类达到效果相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void setListener()
{
scan_bt.setOnClickListener(new Listener1());
history_bt.setOnClickListener(new Listener2());
}

class Listener1 implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub

}
}

class Listener2 implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub

}
}

这种写法虽然能达到一样的效果,但是既冗长又难以维护,所以一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和static修饰符的。

  匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}

class Outter {
public Outter() {

}

static class Inner {
public Inner() {

}
}
}

深入理解内部类

1.为什么成员内部类可以无条件访问外部类的成员?

  在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是Outter.java的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Outter {
private Inner inner = null;
public Outter() {

}

public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}

protected class Inner {
public Inner() {

}
}
}

编译之后,出现了两个字节码文件:

反编译Outter$Inner.class文件得到下面信息:

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
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
SourceFile: "Outter.java"
InnerClass:
#24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // com/cxh/test2/Outter$Inner
const #2 = Asciz com/cxh/test2/Outter$Inner;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz this$0;
const #6 = Asciz Lcom/cxh/test2/Outter;;
const #7 = Asciz <init>;
const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz Code;
const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;// "<init>":()V
const #14 = Asciz ()V;
const #15 = Asciz LineNumberTable;
const #16 = Asciz LocalVariableTable;
const #17 = Asciz this;
const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz SourceFile;
const #20 = Asciz Outter.java;
const #21 = Asciz InnerClasses;
const #22 = class #23; // com/cxh/test2/Outter
const #23 = Asciz com/cxh/test2/Outter;
const #24 = Asciz Inner;

{
final com.cxh.test2.Outter this$0;

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter;
5: aload_0
6: invokespecial #12; //Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 16: 0
line 18: 9

LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/cxh/test2/Outter$Inner;
}

第11行到35行是常量池的内容,下面逐一第38行的内容:
final com.cxh.test2.Outter this$0;

  这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

1
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

  从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

2.为什么局部内部类和匿名内部类只能访问局部final变量?

想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) {

}

public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}

这段代码会被编译成两个class文件:Test.class和Test

1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter
x.class(x为正整数)。


  
  根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。

  上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题:

  当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:

  我们看到在run方法中有一条指令:
bipush 10

  这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

  下面再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {

}

public void test(final int a) {
new Thread(){
public void run() {
System.out.println(a);
};
}.start();
}
}

  反编译得到:

  我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

  也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

  从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?

  对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

  到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

3.静态内部类有特殊的地方吗?

  从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

内部类的使用场景和好处

  为什么在Java中需要内部类?总结一下主要有以下四点:

  1. 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
  2. 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
  3. 方便编写事件驱动程序
  4. 方便编写线程代码

  个人觉得第一点是最重要的原因之一,内部类的存在使得Java的多继承机制变得更加完善。

四.常见的与内部类相关的笔试面试题

1.根据注释填写(1),(2),(3)处的代码

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 class Test{
public static void main(String[] args){
// 初始化Bean1
(1)
bean1.I++;
// 初始化Bean2
(2)
bean2.J++;
//初始化Bean3
(3)
bean3.k++;
}
class Bean1{
public int I = 0;
}

static class Bean2{
public int J = 0;
}
}

class Bean{
class Bean3{
public int k = 0;
}
}

  从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。

  创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
  创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()
  因此,(1),(2),(3)处的代码分别为:

1
2
3
4
5
6
7
8
9
10
11
Test test = new Test();    

  Test.Bean1 bean1 = test.new Bean1();


Test.Bean2 b2 = new Test.Bean2();


Bean bean = new Bean();

Bean.Bean3 bean3 = bean.new Bean3();

2.下面这段代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
Outter outter = new Outter();
outter.new Inner().print();
}
}


class Outter
{
private int a = 1;
class Inner {
private int a = 2;
public void print() {
int a = 3;
System.out.println("局部变量:" + a);
System.out.println("内部类变量:" + this.a);
System.out.println("外部类变量:" + Outter.this.a);
}
}
}

  最后补充一点知识:关于成员内部类的继承问题。一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点:

  1. 成员内部类的引用方式必须为 Outter.Inner.
  2. 构造器中必须有指向外部类对象的引用,并通过这个引用调用super()。这段代码摘自《Java编程思想》
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WithInner {
class Inner{

}
}
class InheritInner extends WithInner.Inner {

// InheritInner() 是不能通过编译的,一定要加上形参
InheritInner(WithInner wi) {
wi.super(); //必须有这句调用
}

public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner obj = new InheritInner(wi);
}
}

参考资料:

  《java编程思想》

  http://www.cnblogs.com/chenssy/p/3388487.html

  http://blog.csdn.net/zhangjg_blog/article/details/20000769

  http://blog.csdn.net/zhangjg_blog/article/details/19996629

  http://blog.csdn.net/zhaoqianjava/article/details/6849812

  http://www.cnblogs.com/nerxious/archive/2013/01/24/2875649.html

作者:海子

出处:http://www.cnblogs.com/dolphin0520/

本博客中未标明转载的文章归作者海子和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利

菜鸟话Java---Object详解

发表于 2019-03-24 | 分类于 java

Object

Object类是Java中所有类(不包括基本数据类型)的超类,在Java中每个类都是由它扩展而来的。如果没有明确指出超类,Object就被
认为是这个类的超类,所以不需要这样显式声明:

1
2
3
public Animal extends Object{

}

所有的数组类型,不管是对象数组还是基本类型的数组都扩展Object类

Object中的常用方法介绍

equals

Object中的equals方法用来检测一个对象是否等于另一个对象。在Object类中
这个方法将判断两个对象是否具有相同的引用。不过一般情况下,这种方式没有用。比如在
现实中我们通常通过比较id的方式来判断员工是否是同一个人。所以,我们都要重写
Object的equals方法。例如:

1
2
3
4
5
6
7
8
9
10
11
public class Employee {

private String id;

...

@Override
public boolean equals(Object obj) {
return id.equals(obj);//String中重写了Object的equals方法,它会比较两个字符串中所有字符是否相等
}
}

equals方法应该具有的特性:

  • 自反性:对于任何非空引用x,x.equals(x)应该返回true
  • 对称性:对于任何引用x和y,当x.equals(y)返回true时,y.equals(x)也应该返回true
  • 传递性:对于任何引用x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true
  • 一致性:如果x和y的引用没有发生变化,反复调用x.equals(y)应该返回相同的值
  • 对于任何非空引用x,x.equals(null)应该返回false

处理继承问题

使用instanceof,例如:

1
if(!(other instanceof Animal))return false;

不过考虑一下这样的情况:

A是B的父类,如果B中重写了equals方法,如下

1
if(!(other instanceof B))return false;

由于:

1
2
A instanceof B 返回 false
B instanceof A 返回 true

所以:

1
2
B.equals(A);返回 false
A.equals(B);返回 true

不符合对称性。要使符合对称性,就必须让父类的equals方法为final.

使用getClass,不检测继承关系。getClass方法只有当两个引用来自同一类时,才会返回true。例如:

1
2
3
4
5
6
7
B.getClass() == A.getClass() 返回false

A.getClass() == B.getClass() 返回false

A.getClass() == A.getClass() 返回true

B.getClass() == B.getClass() 返回true

编写equals()方法的建议:

  1. 显式参数命名为otherObject
  2. 检测this与otherObject是否引用同一个对象。例如:if(this == otherObject)return true;,使用这个就行优化,由于
    计算这个等式要比一个一个比较类中的域所付出的代价要小得多。
  3. 检测otherObject是否为null.例如if(otherObject == null) return false;
  4. 比较this与otherObject是否属于同一个类:

    如果equals被子类覆盖,那么应该使用getClass();

    如果所有的子类都拥有统一的语义,就使用instanceof.
  5. 将otherObject转化为相应类类型的变量ClassName other = (ClassName)otherObject
  6. 对所有需要比较的域就行比较。基本类型使用==,对象域使用equals.

hashCode

散列码(hash code)是由对象导出的一个整数值。散列码是没有规律的。如果x和y是两个不同的
对象,x.hashCode()与y.hashCode基本不会相同。

hashCode()没被覆写时,默认是返回对象的储存地址。

重写hashCode()方法:

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

private String id;

private String name;

...

@Override
public int hashCode() {
return name.hashCode()+id.hashCode()*5;
}
}

注意:equals与hashCode的定义必须一致:如果x.equals(y)返回true那么x.hashCode()就必须等于y.hashCode()

toString

toString方法用来返回对象值的字符串.

Objects

Objects是一个工具类,它提供了很多实用方法,例如:

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
int compare(T a, T b, Comparator c)
如果参数相同,则返回0,否则返回c.compare(a,b)。因此,如果两个参数都为null,则返回0。

boolean deepEquals(Object a, Object b)
检查两个对象是否相等。如果两个参数都相等,则返回true。否则,它返回false。如果两个参数都为null,则返回true。

boolean equals(Object a, Object b)
比较两个对象是否相等。如果两个参数相等,则返回true。否则,它返回false。如果两个参数都为null,则返回true。

int hash(Object... values)
为所有指定的对象生成哈希码。它可以用于计算对象的哈希码,该哈希码基于多个实例字段。

int hashCode(Object o)
返回指定对象的哈希码值。如果参数为null,则返回0。

boolean isNull(Object obj)
如果指定的对象为null,isNull()方法返回true。否则,它返回false。您还可以使用比较运算符==检查对象是否为null,例如,obj == null返回obj的true为null。

boolean nonNull(Object obj)
执行与isNull()方法相反的检查。

T requireNonNull(T obj)
T requireNonNull(T obj, String message)
T requireNonNull(T obj, Supplier messageSupplier)
检查参数是否为null。如果参数为null,它会抛出一个NullPointerException异常。此方法设计用于验证方法和构造函数的参数。
第二个版本可以指定当参数为null时抛出的NullPointerException的消息。
第三个版本的方法将一个Supplier作为第二个参数。

String toString(Object o)
String toString(Object o, String nullDefault)
如果参数为null,则toString()方法返回一个“null”字符串。对于非空参数,它返回通过调用参数的toString()方法返回的值。

Groovy详解(一)

发表于 2019-03-23 | 分类于 Gradle

方法

1
2
3
4
5
def getString(){
println("调用无参方法")
}

println(getString)

结果为:

1
2
调用无参方法
null

Groovy中函数调用的时候还可以不加括号,但是自定义的方法不行。

菜鸟话Java---Random类详解

发表于 2019-03-23 | 分类于 java

JAVA的Random类的用法详解

转载这里

Random类主要用来生成随机数,本文详解介绍了Random类的用法,希望能帮到大家。

Random类中实现的随机算法是伪随机,也就是有规则的随机。在进行随机时,随机算法的起源数字称为种子数(seed),在种子数的基础上进行一定的变换,从而产生需要的随机数字。
相同种子数的Random对象,相同次数生成的随机数字是完全相同的。也就是说,两个种子数相同的Random对象,第一次生成的随机数字完全相同,第二次生成的随机数字也完全相同。这点在生成多个随机数字时需要特别注意。

下面介绍一下Random类的使用,以及如何生成指定区间的随机数组以及实现程序中要求的几率。

Random对象的生成

Random类包含两个构造方法,下面依次进行介绍:

public Random()

该构造方法使用一个和当前系统时间对应的相对时间有关的数字作为种子数,然后使用这个种子数构造Random对象。

public Random(long seed)

该构造方法可以通过制定一个种子数进行创建。

1
2
Random r = new Random();
Random r1 = new Random(10);

再次强调:种子数只是随机算法的起源数字,和生成的随机数字的区间无关。

Random类中的常用方法

Random类中的方法比较简单,每个方法的功能也很容易理解。需要说明的是,Random类中各方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的几率是均等的。下面对这些方法做一下基本的介绍:

1
2
3
4
5
6
7
8
9
10
11
12
a、public boolean nextBoolean()
该方法的作用是生成一个随机的boolean值,生成true和false的值几率相等,也就是都是50%的几率。
b、public double nextDouble()
该方法的作用是生成一个随机的double值,数值介于[0,1.0)之间。
c、public int nextInt()
该方法的作用是生成一个随机的int值,该值介于int的区间,也就是-231到231-1之间。
如果需要生成指定区间的int值,则需要进行一定的数学变换,具体可以参看下面的使用示例中的代码。
d、public int nextInt(int n)
该方法的作用是生成一个随机的int值,该值介于[0,n)的区间,也就是0到n之间的随机int值,包含0而不包含n。
如果想生成指定区间的int值,也需要进行一定的数学变换,具体可以参看下面的使用示例中的代码。
e、public void setSeed(long seed)
该方法的作用是重新设置Random对象中的种子数。设置完种子数以后的Random对象和相同种子数使用new关键字创建出的Random对象相同。

Random类使用示例

使用Random类,一般是生成指定区间的随机数字,下面就一一介绍如何生成对应区间的随机数字。以下生成随机数的代码均使用以下Random对象r进行生成:
Random r = new Random();

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
//生成[0,1.0)区间的小数
double d1 = r.nextDouble();

//生成[0,5.0)区间的小数
double d2 = r.nextDouble() * 5;

//生成[1,2.5)区间的小数
double d3 = r.nextDouble() * 1.5 + 1;

//生成任意整数
int n1 = r.nextInt();


//生成[0,10)区间的整数
int n2 = r.nextInt(10);
n2 = Math.abs(r.nextInt() % 10);

/*以上两行代码均可生成[0,10)区间的整数。
第一种实现使用Random类中的nextInt(int n)方法直接实现。
第二种实现中,首先调用nextInt()方法生成一个任意的int数字,该数字和10取余以后生成的数字区间为(-10,10),然后再对该区间求绝对值,则得到的区间就是[0,10)了。
同理,生成任意[0,n)区间的随机整数,都可以使用如下代码:
*/
int n2 = r.nextInt(n);
n2 = Math.abs(r.nextInt() % n);

//生成[0,10]区间的整数
int n3 = r.nextInt(11);
n3 = Math.abs(r.nextInt() % 11);

//g、生成[-3,15)区间的整数
int n4 = r.nextInt(18) - 3;
n4 = Math.abs(r.nextInt() % 18) - 3;

几率实现

按照一定的几率实现程序逻辑也是随机处理可以解决的一个问题。下面以一个简单的示例演示如何使用随机数字实现几率的逻辑。
在前面的方法介绍中,nextInt(int n)方法中生成的数字是均匀的,也就是说该区间内部的每个数字生成的几率是相同的。那么如果生成一个[0,100)区间的随机整数,则每个数字生成的几率应该是相同的,而且由于该区间中总计有100个整数,所以每个数字的几率都是1%。按照这个理论,可以实现程序中的几率问题。
示例:随机生成一个整数,该整数以55%的几率生成1,以40%的几率生成2,以5%的几率生成3。实现的代码如下:

1
2
3
4
5
6
7
8
9
int n5 = r.nextInt(100);
int m; //结果数字
if(n5 < 55){ //55个数字的区间,55%的几率
m = 1;
}else if(n5 < 95){//[55,95),40个数字的区间,40%的几率
m = 2;
}else{
m = 3;
}

因为每个数字的几率都是1%,则任意55个数字的区间的几率就是55%,为了代码方便书写,这里使用[0,55)区间的所有整数,后续的原理一样。
当然,这里的代码可以简化,因为几率都是5%的倍数,所以只要以5%为基础来控制几率即可,下面是简化的代码实现:

1
2
3
4
5
6
7
8
9
int n6 = r.nextInt(20);
int m1;
if(n6 < 11){
m1 = 1;
}else if(n6 < 19){
m1 = 2;
}else{
m1 = 3;
}

在程序内部,几率的逻辑就可以按照上面的说明进行实现。

其它问题

a、相同种子数Random对象问题

前面介绍过,相同种子数的Random对象,相同次数生成的随机数字是完全相同的,下面是测试的代码:

1
2
3
4
5
6
Random r1 = new Random(10);
Random r2 = new Random(10);
for(int i = 0;i < 2;i++){
System.out.println(r1.nextInt());
System.out.println(r2.nextInt());
}

在该代码中,对象r1和r2使用的种子数都是10,则这两个对象相同次数生成的随机数是完全相同的。
如果想避免出现随机数字相同的情况,则需要注意,无论项目中需要生成多少个随机数字,都只使用一个Random对象即可。

b、关于Math类中的random方法

其实在Math类中也有一个random方法,该random方法的工作是生成一个[0,1.0)区间的随机小数。
通过阅读Math类的源代码可以发现,Math类中的random方法就是直接调用Random类中的nextDouble方法实现的。
只是random方法的调用比较简单,所以很多程序员都习惯使用Math类的random方法来生成随机数字。

菜鸟话Java---Java异常

发表于 2019-03-23 | 分类于 java

异常分类

如图是Java异常层次结构的一个简化图:

所有的异常都由Throwable继承而来,之后分为Error和Exception两部分。

Error

Error类层次结构描述了Java运行时系统内部错误和资源耗尽错误。应用程序不应该抛出
这种类型的错误。对于这种错误我们无能为力,因此我们不需要关注这种错误。

Exception

根据程序是否有问题可以把Exception分成两类:由程序错误导致的异常属于RuntimeException;由
像I/O错误这类和程序无关的问题导致的异常属于其他异常。可以说,如果出现了RuntimeException异常,
那么就一定是你的问题。

Java语言规范将派生于Error和RuntimeException的所有异常称为非受查异常,所以其他的异常称为
受查异常。

异常抛出的情况

  • 调用一个抛出受查异常的方法
  • 程序运行过程发现错误,并利用throw语句抛出一个受查异常
  • 程序出现错误
  • Java虚拟机和运行时库出现内部错误报告

抛出异常

关键字throws或throw用来抛出异常,示例:

1
2
3
4
5
public void deposit(double amount) throws RemoteException
{
// Method implementation
throw new RemoteException();
}

注意:

  • 如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。如果超类
    方法没有抛出任何受查异常,子类也不会抛出任何受查异常。
  • 一旦方法抛出异常,这个方法就不可能返回到调用者

创建异常类

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
//继承检查异常,抛出需要检查
public class MyException extends Exception{

public MyException(String error) {
super(error);
}
}

public static void main(String[] args) {
try {
throw new MyException("出了什么错");
} catch (MyException e) {
e.printStackTrace();
}
}

//继承非受查异常,抛出不需要检查
public class MyException extends RuntimeException{

public MyException(String error) {
super(error);
}
}

public static void main(String[] args) {
throw new MyException("出了什么错");
}

运行结果:

1
2
MyException: 出了什么错
at Main.main(Main.java:6)

捕获异常

使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。
try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:

1
2
3
4
5
6
7
try
{
// 程序代码
}catch(ExceptionName e1)
{
//当发生异常时,会调用这里的代码
}

多重捕获块

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获
多重捕获块的语法如下所示:

1
2
3
4
5
6
7
8
9
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}

可以在 try 语句后面添加任意数量的 catch 块。
如果保护代码中发生异常,异常被抛给第一个 catch 块。
如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。
如果不匹配,它会被传递给第二个 catch 块。
如此,直到异常被捕获或者通过所有的 catch 块。

java7以后可以放在一个catch中,例如:

1
2
3
4
5
try{
// 程序代码
}catch(异常类型1 |异常类型2 异常的变量名){//当捕获多个变量时,异常变量隐含为final变量,不能为它赋值
// 程序代码
}

注意:只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性

异常方法

Throwable 类的主要方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String getMessage()
返回关于发生的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了。

public Throwable getCause()
返回一个Throwable 对象代表异常原因。

public String toString()
使用getMessage()的结果返回类的串级名字。

public void printStackTrace()
打印toString()结果和栈层次到System.err,即错误输出流。

public StackTraceElement [] getStackTrace()
返回一个包含堆栈层次的数组。下标为0的元素代表栈顶,最后一个元素代表方法调用堆栈的栈底。

public Throwable fillInStackTrace()
用当前的调用栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中

finally

finally 关键字用来创建在 try 代码块后面执行的代码块。
无论是否发生异常,finally 代码块中的代码总会被执行。
在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
finally 代码块出现在 catch 代码块最后,语法如下:

1
2
3
4
5
6
7
8
9
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}

注意:当finally子句包含return语句时,在方法返回前,finally子句的内容
会被执行,这里的return语句的返回值会覆盖原始的返回值,例如:

1
2
3
4
5
6
7
8
public static int f(int n){
try{
int r=n*n;
return r;
}finally{
if(n==2)return 0;
}
}

如果调用f(2),那么try语句块的计算结果为r = 4,并执行return语句。
然而,在方法真正返回时,还要执行finally子句。finally子句使得方法返回
0,覆盖了原来的方法。

带资源的try

语法:

1
2
3
try(Resure res = ....){
//具体代码
}

使用带资源的try语句,不用自己关闭资源。

菜鸟话Java---Number接口详解

发表于 2019-03-23 | 分类于 java

Number类

转自这里

大多数时候,在使用java中的数字时,我们使用原始数据类型。但是,Java还在java.lang包中的抽象类Number下提供了各种数字包装子类。Number类下主要有6个子类。这些子类定义了一些在处理数字时经常使用的有用方法。byte, integer, double, short, float, long

为什么要在原始数据上使用Number类对象?

数字类定义的常量(如MIN_VALUE和MAX_VALUE)提供数据类型的上限和下限,非常有用。
Number类对象可以用作期望对象的方法的参数(通常用于处理数字集合)。
类方法可用于将值转换为其他基本类型以及从其他基本类型转换值,用于转换字符串和从字符串转换,以及用于在数字系统(十进制,八进制,十六进制,二进制)之间进行转换。

Number的所有子类通用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
xxx xxxValue():这里xxx表示原始数字数据类型(byte,short,int,long,float,double)。此方法用于将此 Number对象的值转换为指定的基本数据类型。 
句法 :
byte byteValue()
short shortValue()
int intValue()
long longValue()
float floatValue()
double doubleValue()
参数:
----
返回:
此对象表示的数值
转换为指定类型后

示例:

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
//Java program to demonstrate xxxValue() method
public class Test
{
public static void main(String[] args)
{
// Creating a Double Class object with value "6.9685"
Double d = new Double("6.9685");

// Converting this Double(Number) object to
// different primitive data types
byte b = d.byteValue();
short s = d.shortValue();
int i = d.intValue();
long l = d.longValue();
float f = d.floatValue();
double d1 = d.doubleValue();

System.out.println("value of d after converting it to byte : " + b);
System.out.println("value of d after converting it to short : " + s);
System.out.println("value of d after converting it to int : " + i);
System.out.println("value of d after converting it to long : " + l);
System.out.println("value of d after converting it to float : " + f);
System.out.println("value of d after converting it to double : " + d1);
}
}
输出:
value of d after converting it to byte : 6
value of d after converting it to short : 6
value of d after converting it to int : 6
value of d after converting it to long : 6
value of d after converting it to float : 6.9685
value of d after converting it to double : 6.9685

注意:转换时,可能会发生精度损失。例如,我们可以看到在从Double对象转换为int数据类型时,小数部分(“.9685”)已被省略。

菜鸟话Java---Java接口

发表于 2019-03-23 | 分类于 java

接口概述

接口不是类,而是对类的一组需求描述。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口的声明

1
2
3
4
5
6
//接口的声明语法格式如下:[]括起来的表示可有可无

访问修饰符 interface 接口名称 [extends 其他的接口名名] {
// 声明变量
// 抽象方法
}

接口的实现

接口的实现要使用Implements关键字,并且要实现接口中所有定义的方法

接口的特点

  • 不能使用new实例化一个接口
  • 接口变量必须引用实现了接口的变量
  • 接口中不能包含成员变量和静态方法(java 8允许),但可以包含常量
  • 接口中所有的方法都自动设置为public,所有的域都自动设置为public static final
  • 接口可以多继承

接口和抽象类的区别

  1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

默认方法

Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
我们只需在方法名前面加个 default 关键字即可实现默认方法。

为什么要有这个特性?

首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

1
2
3
4
5
6
默认方法语法格式如下:
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}

多个默认方法

一个接口有默认方法,考虑这样的情况,一个类实现了多个接口,且这些接口有相同的默认方法,以下实例说明了这种情况的解决方法:

1
2
3
4
5
6
7
8
9
10
11
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
}

public interface FourWheeler {
default void print(){
System.out.println("我是一辆四轮车!");
}
}

第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法:

1
2
3
4
5
public class Car implements Vehicle, FourWheeler {
default void print(){
System.out.println("我是一辆四轮汽车!");
}
}

第二种解决方案可以使用 super 来调用指定接口的默认方法:

1
2
3
4
5
public class Car implements Vehicle, FourWheeler {
public void print(){
Vehicle.super.print();
}
}

静态默认方法

Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法。例如:

1
2
3
4
5
6
7
8
9
public interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}
// 静态方法
static void blowHorn(){
System.out.println("按喇叭!!!");
}
}

默认方法实例

我们可以通过以下代码来了解关于默认方法的使用,可以将代码放入 Java8Tester.java 文件中:

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
Java8Tester.java 文件
public class Java8Tester {
public static void main(String args[]){
Vehicle vehicle = new Car();
vehicle.print();
}
}

interface Vehicle {
default void print(){
System.out.println("我是一辆车!");
}

static void blowHorn(){
System.out.println("按喇叭!!!");
}
}

interface FourWheeler {
default void print(){
System.out.println("我是一辆四轮车!");
}
}

class Car implements Vehicle, FourWheeler {
public void print(){
Vehicle.super.print();
FourWheeler.super.print();
Vehicle.blowHorn();
System.out.println("我是一辆汽车!");
}
}

类优先

情景1

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

private String name;

public String getName() {
System.out.println("people的getName调用");
return name;
}
}

public interface Named {
String getName();
}

public class Test extends People implements Named {//当超类和接口有重名的方法时,不需要子类实现接口方法

}

情景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
public class People {

private String name;

public String getName() {
System.out.println("people的getName调用");
return name;
}
}


public interface Named {
default String getName(){
System.out.println("默认方法的getName调用");
return "";
}
}

public class Test extends People implements Named {//当超类和接口的默认方法重名时,忽略接口的默认方法,即类优先

}

public static void main(String[] args) {
Test t = new Test();
t.getName();//输出为:people的getName调用
}

注意:千万不能让一个默认方法重新定义Object类中的某个方法,例如,toString。由于类优先原则,这样的
方法绝对无法超越Object.toString.

菜鸟话Java---包装类

发表于 2019-03-23 | 分类于 java

包装类

Java有八种基本数据类型不支持面向对象,这可能造成不便。所以为解决此类问题 ,Java为每种基本数据类型分别设计了对应的类,称之为包装类

基本类型 对应的包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

自动装箱

1
2
3
4
5
6
7
int m = 500;
Integer obj = m;

//编译器会把上面自动转化为

int m = 500;
Integer obj = Integer.valueOf(m);

自动拆箱

1
2
3
4
5
6
7
Integer obj = new Integer(12);
int n = obj;

//编译器会把上面转化为

Integer obj = new Integer(12);
int n = obj.intValue();

注意:装箱和拆箱是编译器认可的,而不是虚拟机

菜鸟话Java---String的使用

发表于 2019-03-22 | 分类于 java

创建字符串

String 类有 11 种构造方法,这些方法提供不同的参数来初始化字符串,下面是常用的:

1
2
3
4
5
6
7
8
9
//直接创建
String str1 = "这个字符串";
//使用字符串创建
String str2 = new String("这个字符串");
//使用数组
char[] helloArray = { 'r', 'u', 'n', 'o', 'o', 'b'};
String helloString = new String(helloArray);

...

当n个字符串直接由+连接起来组成新的字符串时,不是分别创建n+1个String对象。而是直接将合成一个字符串,即只创建了一个字符串对象

1
String a = "abc"+"123"+"567";

当分别创建字符串变量,然后使用+连接起来时

1
2
3
4
5
6
7
8
String a = "abc";
String a1 = "123";
String b1 = a+a1;

//会转化为
String a = "abc";
String a1 = "123";
(new StringBuilder()).append(a).append(a1).toString();

注意:String是不可变字符串,如果需要对字符串做很多修改,那么应该选择使用 StringBuffer 和 StringBuilder 类

String的常用方法

length()

返回字符串的长度

concat

连接字符串,例如string1.concat(string2);

char charAt(int index)

返回指定索引处的 char 值。

boolean endsWith(String suffix)

测试此字符串是否以指定的后缀结束。

indexOf(…)

1
2
3
4
5
6
7
8
9
10
11
int indexOf(int ch)
返回指定字符在此字符串中第一次出现处的索引。

int indexOf(int ch, int fromIndex)
返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。

int indexOf(String str)
返回指定子字符串在此字符串中第一次出现处的索引。

int indexOf(String str, int fromIndex)
返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。

intern()

转载这里

  今天在看一本书的时候注意到一个String的intern()方法,平常没用过,只是见过这个方法,也没去仔细看过这个方法。所以今天看了一下。个人觉得给String类中加入这个方法可能是为了提升一点点性能,因为从常量池取数据比从堆里面去数据要快一些。(个人感觉)

  API上的那几句关于这个方法,其实总结一句就是调用这个方法之后把字符串对象加入常量池中,常量池我们都知道他是存在于方法区的,他是方法区的一部分,而方法区是线程共享的,所以常量池也就是线程共享的,但是他并不是线程不安全的,他其实是线程安全的,他仅仅是让有相同值的引用指向同一个位置而已,如果引用值变化了,但是常量池中没有新的值,那么就会新开辟一个常量结果来交给新的引用,而并非像线程不同步那样,针对同一个对象,new出来的字符串和直接赋值给变量的字符串存放的位置是不一样的,前者是在堆里面,而后者在常量池里面,另外,在做字符串拼接操作,也就是字符串相”+”的时候,得出的结果是存在在常量池或者堆里面,这个是根据情况不同不一定的,我写了几行代码测试了一下。

先上结果:

  1. 直接定义字符串变量的时候赋值,如果表达式右边只有字符串常量,那么就是把变量存放在常量池里面。
  2. new出来的字符串是存放在堆里面。
  3. 对字符串进行拼接操作,也就是做”+”运算的时候,分2中情况:

i. 表达式右边是纯字符串常量,那么存放在栈里面。

ii. 表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。

1
2
3
4
5
6
7
8
    String str1 = "aaa";
String str2 = "bbb";
String str3 = "aaabbb";
String str4 = str1 + str2;
String str5 = "aaa" + "bbb";
System.out.println(str3 == str4); // false
System.out.println(str3 == str4.intern()); // true
System.out.println(str3 == str5);// true

结果:str1、str2、str3、str5都是存在于常量池,str4由于表达式右半边有引用类型,所以str4存在于堆内存,而str5表达式右边没有引用类型,是纯字符串常量,就存放在了常量池里面。

lastIndexOf(…)

1
2
3
4
5
6
7
8
9
10
11
int (int ch)
返回指定字符在此字符串中最后一次出现处的索引。
int lastIndexOf
int lastIndexOf(int ch, int fromIndex)
返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索。

int lastIndexOf(String str)
返回指定子字符串在此字符串中最右边出现处的索引。

int lastIndexOf(String str, int fromIndex)
返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。

boolean matches(String regex)

告知此字符串是否匹配给定的正则表达式

replaceXXX()

1
2
3
4
5
6
7
8
9

String replace(char oldChar, char newChar)
返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

String replaceAll(String regex, String replacement)
使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

String replaceFirst(String regex, String replacement)
使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

split()

转载菜鸟教程

split() 方法根据匹配给定的正则表达式来拆分字符串。

注意: . 、 | 和 * 等转义字符,必须得加 \。

注意:多个分隔符,可以用 | 作为连字符。

语法:

1
2
3
4
5
6
public String[] split(String regex, int limit)
参数
regex -- 正则表达式分隔符。
limit -- 分割的份数。
返回值
字符串数组。

实例:

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
public class Test {
public static void main(String args[]) {
String str = new String("Welcome-to-Runoob");

System.out.println("- 分隔符返回值 :" );
for (String retval: str.split("-")){
System.out.println(retval);
}

System.out.println("");
System.out.println("- 分隔符设置分割份数返回值 :" );
for (String retval: str.split("-", 2)){
System.out.println(retval);
}

System.out.println("");
String str2 = new String("www.runoob.com");
System.out.println("转义字符返回值 :" );
for (String retval: str2.split("\\.", 3)){
System.out.println(retval);
}

System.out.println("");
String str3 = new String("acount=? and uu =? or n=?");
System.out.println("多个分隔符返回值 :" );
for (String retval: str3.split("and|or")){
System.out.println(retval);
}
}
}

以上程序执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 分隔符返回值 :
Welcome
to
Runoob

- 分隔符设置分割份数返回值 :
Welcome
to-Runoob

转义字符返回值 :
www
runoob
com

多个分隔符返回值 :
acount=?
uu =?
n=?

String trim()

返回字符串的副本,忽略前导空白和尾部空白。

substring(…)

1
2
3
4
5
String substring(int beginIndex)
返回一个新的字符串,它是此字符串的一个子字符串。

String substring(int beginIndex, int endIndex)
返回一个新字符串,它是此字符串的一个子字符串。

更多Java String的方法见api

1…4567
lichukuan

lichukuan

62 博客
12 分类
© 2019 lichukuan
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4