getter和setter在Java中得到了广泛的应用。它看起来很简单,但并不是每个程序员都能正确地理解和实现这种方法。因此,在本文中,我想深入讨论Java中的getter和setter方法-从基础到常见错误和最佳实践。
如果您已经很好地掌握了基础知识,那么直接跳到第4节,在其中我将讨论常见的错误和最佳实践。
1.getter和Setter是什么?
在Java中,getter和setter是用于检索和更新变量值的两种传统方法。
下面的代码是一个带有私有变量的简单类和两个getter/setter方法的示例:
public class SimpleGetterAndSetter {
private int number;
public int getNumber() {
return this.number;
}
public void setNumber(int num) {
this.number = num;
}
}
该类声明一个私有变量Number。由于Number是私有的,该类外部的代码不能直接访问变量,如下所示:
SimpleGetterAndSetter obj = new SimpleGetterAndSetter();
obj.number = 10; // compile error, since number is private
int num = obj.number; // same as above
相反,外部代码必须调用getter,getNumber()和策划人,setNumber(),以便读取或更新变量,例如:
SimpleGetterAndSetter obj = new SimpleGetterAndSetter();
obj.setNumber(10); // OK
int num = obj.getNumber(); // fine
因此,setter是一个更新变量值的方法。getter是读取变量值的方法。getter和setter也称为存取者和突变体在JAVA。
2.为什么我们需要getter和Setter?
通过使用getter和setter,程序员可以控制如何以适当的方式访问和更新它们的重要变量,例如在指定的范围内更改变量的值。考虑一下setter方法的以下代码:
public void setNumber(int num) {
if (num < 10 || num > 100) {
throw new IllegalArgumentException();
}
this.number = num;
}
这确保了数字的值始终设置在10到100之间。假设变量号可以直接更新,调用方可以为其设置任意值:
obj.number = 3;
这违反了该变量的值范围为10到100的约束。当然,我们不希望这种情况发生。因此,将变量号隐藏为私有,然后使用setter来挽救。
另一方面,getter方法是外部世界读取变量值的唯一方法:
public int getNumber() {
return this.number;
}
下图说明了情况:
到目前为止,setter和getter方法保护变量的值不受外部世界(调用方代码)意外变化的影响。
当变量被私有修饰符并且只能通过getter和setter访问,它是封装. 封装是面向对象编程(OOP)的基本原则之一,因此实现getter和setter是在程序代码中强制封装的方法之一。
一些框架,如冬眠, 春天,和支柱可以通过getter和setter检查信息或注入它们的实用程序代码。因此,在将代码与此类框架集成时,提供getter和setter是必要的。
3.getter和Setter命名公约
setter和getter的命名方案应遵循Javabean命名约定如getXxx()和setXxx(),在哪里Xxx变量的名称。例如,具有以下变量名:
private String name;
适当的设置者和getter将是:
public void setName(String name) { }
public String getName() { }
如果变量是布尔型的,那么getter的名称可以是isXXX()或getXXX(),但前者的命名是首选的。例如:
private boolean single;
public String isSingle() { }
4.在实现getter和Setter时常见的错误
人们经常犯错误,开发人员也不例外。本节描述了在Java中实现setter和getter时最常见的错误以及解决方法。
错误1:您有setter和getter,但是变量在一个限制较少的范围内声明。
考虑以下代码片段:
public String firstName;
public void setFirstName(String fname) {
this.firstName = fname;
}
public String getFirstName() {
return this.firstName;
}
变量firstName被宣布为公众,因此可以使用点(.)访问它。直接操作,使设置器和吸气器无用。解决此情况的方法是使用更多的受限访问修饰符,如受保护和私:
private String firstName;
错误2:在setter中直接分配对象引用
考虑到以下设置者方法:
private int[] scores;
public void setScores(int[] scr) {
this.scores = scr;
}
以下代码演示了此问题:
int[] myScores = {5, 5, 4, 3, 2, 4};
setScores(myScores);
displayScores();
myScores[1] = 1;
displayScores();
一个整数数组,myScores,则使用6个值(第1行)初始化数组,并将数组传递给setScores()方法(第2行)。方法displayScores()只需打印数组中的所有分数:
public void displayScores() {
for (int i = 0; i < this.scores.length; i++) {
System.out.print(this.scores[i] + " ");
}
System.out.println();
}
第3行将产生以下输出:
5 5 4 3 2 4
这些都是myScores阵列。现在,在第4行中,我们可以修改2的值。Nd元素中的myScores数组如下:
myScores[1] = 1;
如果我们调用这个方法会发生什么?displayScores() 又在5号线?那么,它将产生以下输出:
5 1 4 3 2 4
你知道2的价值Nd元素从5更改为1,这是第4行中赋值的结果。为什么这很重要?这意味着数据可以在setter方法的作用域之外进行修改,这破坏了setter的封装目的。为什么会发生这种事?让我们看看setScores()方法再次:
public void setScores(int[] scr) {
this.scores = scr;
}
成员变量分数分配给方法的参数变量scr直接。这意味着这两个变量都指向内存中的同一个对象-myScores数组对象。因此,对scores或myScores变量实际上是在同一个对象上创建的。
此情况的解决方法是从scr数组到scores阵列,一个接一个。修改后的setter版本如下所示:
public void setScores(int[] scr) {
this.scores = new int[scr.length];
System.arraycopy(scr, 0, this.scores, 0, scr.length);
}
有什么关系呢?成员变量scores不再引用scr变量。相反,数组scores初始化为大小等于数组大小的新数组。scr…然后,我们从数组中复制所有元素。scr到数组scores,使用System.arraycopy()方法。
再次运行以下示例,它将为我们提供以下输出:
5 5 4 3 2 4
5 5 4 3 2 4
现在,两个调用displayScores()生产同样的输出。这意味着数组scores是独立的,并且与数组不同。scr传递给策划人,这样我们就有了任务:
myScores[1] = 1;
这不影响数组。scores.
因此,经验法则是:如果将对象引用传递给setter方法,则不要将该引用直接复制到内部变量中。相反,您应该找到一些方法将传递的对象的值复制到内部对象中,就像我们使用System.arraycopy()方法。
错误3:在getter中直接返回对象引用
考虑以下getter方法:
private int[] scores;
public int[] getScores() {
return this.scores;
}
然后查看以下代码片段:
int[] myScores = {5, 5, 4, 3, 2, 4};
setScores(myScores);
displayScores();
int[] copyScores = getScores();
copyScores[1] = 1;
displayScores();
它将产生以下产出:
5 5 4 3 2 4
5 1 4 3 2 4
正如你注意到的,2Nd数组的元素scores由于getter方法直接返回内部变量的引用,外部代码可以获得此引用并对内部对象进行更改。
解决这个问题的方法是,我们应该返回对象的副本,而不是直接返回getter中的引用。这是为了使外部代码只能获得副本,而不是内部对象。因此,我们将上述getter修改如下:
public int[] getScores() {
int[] copy = new int[this.scores.length];
System.arraycopy(this.scores, 0, copy, 0, copy.length);
return copy;
}
因此,经验法则是:不要在getter方法中返回原始对象的引用。相反,它应该返回原始对象的副本。
5.为基本类型实现getter和Setter
与原语类型(int, float, double, boolean, char(…),您可以在setter/getter中直接分配/返回值,因为Java将一个原语的值复制到另一个原语,而不是复制对象引用。因此,错误#2和#3是很容易避免的。
例如,以下代码是安全的,因为setter和getter包含在float:
private float amount;
public void setAmount(float amount) {
this.amount = amount;
}
public float getAmount() {
return this.amount;
}
因此,对于基本类型,没有正确实现getter和setter的特殊技巧。
6.为公共对象类型实现getter和Setter
字符串对象的getter和Setters:
String是一个对象类型,但它是不可变的,这意味着一旦创建了一个字符串对象,它的字符串文本就不能被更改。换句话说,对该字符串对象的每次更改都将导致新创建的String对象。因此,就像基本类型一样,您可以安全地为字符串变量实现getter和setter,如下所示:
private String address;
public void setAddress(String addr) {
this.address = addr;
}
public String getAddress() {
return this.address;
}
日期对象的getter和Setter:
这个java.util.Date类实现clone()方法的Object班级,等级。方法clone()返回对象的副本,因此我们可以将其用于getter和setter,如下面的示例所示:
private Date birthDate;
public void setBirthDate(Date date) {
this.birthDate = (Date) date.clone();
}
public Date getBirthDate() {
return (Date) this.birthDate.clone();
}
7.为集合类型实现getter和Setter
正如错误#2和#3所描述的那样,设置setter和getter方法是不好的:
rivate List<String> listTitles;
public void setListTitles(List<String> titles) {
this.listTitles = titles;
}
public List<String> getListTitles() {
return this.listTitles;
}
考虑以下方案:
import java.util.*;
public class CollectionGetterSetter {
private List<String> listTitles;
public void setListTitles(List<String> titles) {
this.listTitles = titles;
}
public List<String> getListTitles() {
return this.listTitles;
}
public static void main(String[] args) {
CollectionGetterSetter app = new CollectionGetterSetter();
List<String> titles1 = new ArrayList();
titles1.add("Name");
titles1.add("Address");
titles1.add("Email");
titles1.add("Job");
app.setListTitles(titles1);
System.out.println("Titles 1: " + titles1);
titles1.set(2, "Habilitation");
List<String> titles2 = app.getListTitles();
System.out.println("Titles 2: " + titles2);
titles2.set(0, "Full name");
List<String> titles3 = app.getListTitles();
System.out.println("Titles 3: " + titles3);
}
}
根据执行getter和setter的规则,System.out.println()语句应该产生相同的结果。但是,当运行上述程序时,它会产生以下输出:
Titles 1: [Name, Address, Email, Job]
Titles 2: [Name, Address, Habilitation, Job]
Titles 3: [Full name, Address, Habilitation, Job]
对于String集合,一种解决方案是使用以另一个集合作为参数的构造函数。例如,我们可以将上面的getter和setter的代码修改如下:
public void setListTitles(List<String> titles) {
this.listTitles = new ArrayList<String>(titles);
}
public List<String> getListTitles() {
return new ArrayList<String>(this.listTitles);
}
重新编译并运行CollectionGetterSetter程序;它将产生所需的输出:
Titles 1: [Name, Address, Email, Job]
Titles 2: [Name, Address, Email, Job]
Titles 3: [Name, Address, Email, Job]
注*上面的构造函数方法仅用于弦,但它不适用于集合对象…的集合,请考虑下面的示例。Person目的:
import java.util.*;
public class CollectionGetterSetterObject {
private List<Person> listPeople;
public void setListPeople(List<Person> list) {
this.listPeople = new ArrayList<Person>(list);
}
public List<Person> getListPeople() {
return new ArrayList<Person>(this.listPeople);
}
public static void main(String[] args) {
CollectionGetterSetterObject app = new CollectionGetterSetterObject();
List<Person> list1 = new ArrayList<Person>();
list1.add(new Person("Peter"));
list1.add(new Person("Alice"));
list1.add(new Person("Mary"));
app.setListPeople(list1);
System.out.println("List 1: " + list1);
list1.get(2).setName("Maryland");
List<Person> list2 = app.getListPeople();
System.out.println("List 2: " + list2);
list1.get(0).setName("Peter Crouch");
List<Person> list3 = app.getListPeople();
System.out.println("List 3: " + list3);
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return this.name;
}
}
它在运行时产生以下输出:
List 1: [Peter, Alice, Mary]
List 2: [Peter, Alice, Maryland]
List 3: [Peter Crouch, Alice, Maryland]
因为与字符串不同,每当复制字符串对象时,都会为字符串创建新对象,其他Object类型不是。只有引用被复制,所以这就是为什么两个集合是不同的,但是它们包含相同的对象。换句话说,这是因为我们没有提供任何复制对象的方法。
查看Collection API;我们发现ArrayList, HashMap, HashSet等实施自己的clone()方法。这些方法返回浅拷贝,这些副本不将元素从源集合复制到目标。根据Javadocclone()方法ArrayList班级:
因此,我们不能使用clone()这些集合类的方法。解决方案是实现clone()方法为我们自己定义的对象-Person在上面的示例中初始化。我们实现clone()方法中的Person类,如下所示:
public Object clone() {
Person aClone = new Person(this.name);
return aClone;
}
8.为您自己的类型实现getter和Setter
如果定义自定义对象类型,则应实现clone()方法用于您自己的类型。例如:
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return this.name;
}
public Object clone() {
Person aClone = new Person(this.name);
return aClone;
}
}
因此,为自定义对象类型实现getter和setter的规则如下:
实现clone()方法,用于自定义类型。
从getter返回一个克隆的对象。
在setter中分配一个克隆对象。
结语
Java getter和setter看起来很简单,但是如果简单地实现它可能会很危险。它甚至可能是导致代码行为不当的问题的根源。或者更糟糕的是,人们可以很容易地利用您的程序,通过隐秘地将参数操作到getter和setter并从其返回对象。因此,要小心,并考虑实现上述最佳实践。