获取器和设置器有什么作用?

85

可能是重复问题:
为什么要使用getter和setter?

我读过关于Java的书,其中提到为变量(如xy)创建getter和setter方法是很好的做法。例如:

public int getX(){
    return x;
}

public void setX(int x){
    this.x = x;
}

但是这和那之间的区别是什么呢?

...(shape.x)...   // Basically getX()

shape.x = 90;    // Basically setX()

如果说setter和getter方法更好,那么会出现哪些实际问题?


2
请查看此链接:https://dev59.com/oHI_5IYBdhLWcg3wAeHl#1568230 - Zaz Gmy
4
事实上,访问器是一种巨大的负担,你应该在每种情况下权衡成本与收益。有些情况下,公共字段(特别是如果你可以将它们设为 final)是正确的选择。 - Marko Topolnik
在最佳实践的神圣名义下完全是胡说八道。 - MonoThreaded
12个回答

91

有多种原因:

  • 如果允许像

    shape.x = 90

这样的字段访问方式,那么以后就无法添加任何验证数据的逻辑。

比如说,如果x不能小于100,你就不能这样做,但是如果你使用了像下面这样的setter:

public void setShapeValue(int shapeValue){
  if(shapeValue < 100){
    //do something here like throw exception.
  }
}
  • 您不能添加类似于写时复制逻辑(请参见CopyOnWriteArrayList)的内容。
  • 另一个原因是要访问类外部的字段,您将不得不将它们标记为public、protected或default,这样您就失去了控制。当数据非常内部化到类中时,会破坏封装和一般的面向对象编程方法论。

对于像常量这样的值

public final String SOMETHING = "SOMETHING";

如果你想让字段不能被更改,你需要允许对它们的访问,例如实例变量会使用getter和setter来代替。

  • 另一种情况是当你想要让类成为不可变的时,如果你允许字段访问,那么你就破坏了类的不可变性,因为值可以被更改。但是如果你仔细设计类并只提供getter而没有setter,你就可以保持不可变性。

尽管在这种情况下,你必须在getter方法中小心处理,以确保不会泄漏对象的引用(如果你的类具有对象实例)。

我们可以使用getter和setter来在任何包中访问私有变量。


1
99% 的情况下,“写时复制”都没有意义。想象一下在 shape.x 上使用“写时复制”... - Pacerier
2
如果需要在getter/setter内部添加其他逻辑,那么是否应该能够将公共字段成员重构为私有字段并使用getter/setter?从字面上来说,setter和getter似乎是“稍后编写代码行”的过早优化。从一开始就编写getter/setter的唯一原因是,如果您想将此接口公开给某些您无法控制且不希望破坏向后兼容性的代码。 - andrybak
1
你可以在主代码中使用if语句或validate函数来验证数据。 - DarmaniLink
你的第一个观点涉及到为什么这是默认做法的问题:为什么在没有明显原因(比如不可变性)的情况下从一开始就这样做?目前看来,似乎归结为未雨绸缪,或者添加一层间接性,以防万一你将来需要解决一个你现在没有的问题。对于将被第三方代码使用且你无法控制的类可能是有效的,但我并不完全相信。 - Guildenstern
好的解释和示例 - MK.

12

使用getter和setter函数可以实现约束和封装。比如说x是半径,shape.x = -10就没有太多意义。此外,如果有人尝试设置一个非法值,你可以打印一个错误、设置一个默认值或者什么也不做。

将成员变量设为私有是一种好的实践,这样它们就不能被直接修改。

Mutator函数
封装


是的,它们允许这样做,但大多数情况下(根据我的经验),它们只是简单地进行获取/设置操作。并且这些字段后来通常不需要约束条件。(大多数情况下) - Guildenstern

6
很多人提到了封装实现的具体细节,这对我来说是使用类中的getter和setter方法的最大原因。通过这种方式,您还可以获得许多其他好处,包括能够随意抛弃和替换实现而无需触及使用您的类的每个代码片段。在小项目中,这并不是一个很大的好处,但如果您的代码最终成为广泛使用的(内部或公共)库,它可能会带来巨大的好处。
一个具体的例子是数学中的复数。一些语言将其作为语言或框架功能,而另一些则没有。我将在此处使用可变类作为示例,但同样可以使用不可变类。
一个复数可以写成形式为a + bi的实部和虚部,非常适合使用getRealPart和setRealPart以及getImaginaryPart和setImaginaryPart。
然而,在某些情况下,更容易理解极坐标形式的复数re^(iθ),其中r使用getRadius和setRadius表示,而θ使用getAngle和setAngle表示。
您还可以公开诸如setComplexNumber(realPart,imaginaryPart)和setComplexNumber(radius,angle)之类的方法。根据参数类型,这些方法可能需要不同的名称,但类的消费者可以根据自身需求使用任何一种。
这两种形式是可以互相转换的;您可以相当容易地从一种形式转换为另一种形式,因此类使用哪种形式进行内部存储对于该类的使用者来说是无关紧要的。但是,使用a+bi形式作为内部表示,并且不使用getter和setter而是直接公开字段,不仅会强制类使用者使用该形式,而且您也不能随后轻松更改主意并用re^(iθ)替换内部表示,因为这在您特定的情况下更容易实现。您被限制使用您定义的公共API,该API要求明确地使用特定字段名称公开实部和虚部。

这也适用于像测量单位或绝对与相对坐标之类的事物。 - Jeanne Pindar

4

使用 gettersetter 的另一个好处可以通过以下示例理解

public class TestGetterSetter{
    private String name ;


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


    public String getName(){
        return this.name ;
    }
}

获取器和设置器的作用是只有它们被用于访问私有变量,从而提供封装性,这样以后重构或修改代码会更容易。如果你使用名称代替getter,那么如果你想添加一些默认值(比如说默认名称是“Guest”),那么你就必须同时修改getter和sayName函数。
public class TestGetterSetter{
    private String name ;


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


    public String getName(){
        if (this.name == null ){
            setName("Guest");
        }
        return this.name ;
    }
}

不要求 getter 和 setter 必须以 get 和 set 开头 - 它们只是普通的成员函数。然而,这是一个常规约定。(特别是如果你使用 Java Beans)


6
Getter和setter是重构过程中最麻烦的部分!例如,试着将你50个属性的JavaBean中的30个String类型字段改为Date类型字段。 - Marko Topolnik
1
如果你想设置一个默认值,只需使用构造函数!打开你的Java思维! - David Lemon
这实际上是访问器完全不必要的一个很好的例子。如果我可以设置名称并获取名称,那就将其设为公共的。 - GaloisGirl

4

我认为使用getter和setter的最好理由之一是类API的永久性。在像Python这样的语言中,您可以通过名称访问成员并稍后将它们更改为方法。但是,在Java中,函数与成员的行为不同,一旦访问了属性,就没法再限制其作用域,否则会破坏客户端。

通过提供getter和setter,程序员可以自由地修改成员和行为,只要遵守公共API所描述的合同即可。


3
假设你找到了一个比你自己的类(YourClass)做得更好的库。此时自然而然的事情是将YourClass作为该库的包装接口。它仍然有“X”的概念,您的客户端代码需要获取或设置它。在这一点上,您几乎必须编写访问器函数。
如果您忽略使用访问器函数并允许客户端代码直接访问YourClass.x,则现在必须重写所有曾经触及YourClass.x的客户端代码。但是,如果您从一开始就使用YourClass.getX()和YourClass.setX(),则只需要重写YourClass。
编程,特别是面向对象编程的一个关键概念,是隐藏实现细节,使其不被其他类或模块中的代码直接使用。这样,如果您更改实现细节(如上例),客户端代码不会知道差异,也不必修改。对于所有客户端代码来说,“x”可能是一个变量,也可能是即时计算出的值。
这只是一种过度简化,并不能涵盖所有隐藏实现有益的场景,但它是最明显的例子。隐藏实现细节的概念现在与OOP密切相关,但是在OOP出现之前数十年就有了讨论。它回归到软件开发的核心概念之一,即将一个大而模糊的问题分解为小而明确的问题,这些问题可以轻松解决。访问器函数有助于保持您的小子任务分离和明确定义:类之间了解彼此内部情况越少,就越好。

1

有很多原因。以下只是其中几个。

  1. 访问器,特别是getter,经常出现在接口中。你不能在接口中规定成员变量。
  2. 一旦你公开了这个成员变量,你就不能改变它的实现方式。例如,如果你后来发现需要切换到像聚合这样的模式,你希望“x”属性实际上来自某个嵌套对象,那么你最终不得不复制该值并尝试保持同步。这不好。
  3. 大多数情况下,你最好不要公开setter。你不能用像x这样的公共字段做到这一点。

1
在回答问题之前,我们需要先了解一些东西...!"JavaBeans"。
JavaBeans是具有属性的Java类。对于我们的目的,将属性视为私有实例变量。由于它们是私有的,因此从类外部访问它们的唯一方法是通过类中的“方法”。
更改属性值的方法称为setter方法,检索属性值的方法称为getter方法

0

这是通过应用面向对象编程的封装原则来实现的。

一种限制访问对象某些组件的语言机制。

这意味着,您必须为类的属性和方法定义可见性。有三种常见的可见性:

  • 私有:只有类可以看到和使用属性/方法。
  • 受保护的:只有类及其子类可以看到和使用属性/方法。
  • 公共的:每个类都可以看到和使用属性/方法。

当您声明私有/受保护的属性时,建议创建获取值(get)和更改值(set)的方法。一个关于可见性的例子是[ArrayList][2]类:它有一个size属性来知道内部数组的实际大小。只有类必须更改其值,因此代码如下:

public class ArrayList<E> {
    private int size;
    private Object[] array;
    public getSize() {
        return this.size;
    }
    public void add(E element) {
        //logic to add the element in the array...
        this.size++;
    }
}

在这个例子中,你可以看到 size 值只能在类方法内部改变,并且你可以通过在代码中调用它来获取实际的大小(而不是改变它)。
public void someMethod() {
    List<String> ls = new ArrayList<String>();
    //adding values
    ls.add("Hello");
    ls.add("World");
    for(int i = 0; i < ls.size(); i++) {
        System.out.println(ls.get(i));
    }
}

0
我认为既不是getter/setter也不是公共成员是良好的面向对象设计。它们都通过将对象的数据暴露给世界来破坏了OOP封装,而这些数据可能本来就不应该访问对象的属性。

你说得对,这是封装的一个例子,但并不是本意。然而,你必须考虑到我们实际上只需要简单的结构体的现实情况。当JavaBeans被定义时,PropertyChangeListeners是设计的一个重要部分--JavaBeans的设计主要是以Swing为主要考虑因素的。如果你想要可观察的属性更改,你无法避免使用getter和setter。 - Marko Topolnik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接