公共字段的替代方案是什么?

13

我正在使用Java编写一个游戏,正如问题标题所示,我在我的类中使用公共字段。(暂时如此)

从我所看到的,公共字段是不好的,我也有一些理解为什么。(但如果有人能澄清为什么不应该使用它们,那将不胜感激)

问题在于,也从我所看到的(并且似乎很合理)是,使用私有字段,但使用getter和setter访问它们也不好,因为这违反了使用私有字段的初衷。

那么,我的问题是,有什么替代方案吗?或者我真的必须使用带有getter和setter的私有字段吗?

以下是其中一个类及其部分方法的参考。

如有需要,我可以进行更详细的说明。

public double health;
//The player's fields.
public String name;
public double goldCount;
public double maxWeight;
public double currentWeight;
public double maxBackPckSlts;
public double usedBackPckSlts; // The current back pack slots in use
public double maxHealth; // Maximum amount of health
public ArrayList<String> backPack = new ArrayList<String>();

//This method happens when ever the player dynamically takes damage(i.e. when it is not scripted for the player to take damage.
//Parameters will be added to make it dynamic so the player can take any spread of damage.
public void beDamaged(double damage)
{
    this.health -= damage;
    if (this.health < 0)
    {
        this.health = 0;
    }
}

编辑:为了检查目的,这是我的Weapon类现在的样子:(代码示例出现问题,因此看起来不正确。)

private final double DAMAGE;
private final double SPEED;

public Weapon(double initialDmg,double initialSpd,String startName,double initialWg)
{
    DAMAGE = initialDmg;
    SPEED = initialSpd;
    setItemName(startName);
    setItemWeight(initialWg);
}

public double getSpeed() 
{
    return SPEED;
}


public double getDamage()
{
    return DAMAGE;
}

你可以看到,由于WeaponDAMAGESPEED不需要更改,它们可以暂时使用final关键字。 (如果在游戏后期决定升级这些值,可以添加带验证的setter方法,或者创建一个新的具有升级值的武器)。这些值在Weapon构造函数中设置。

总之,当需要时并且明智地使用时,getter和setter是可以的。(但是)


7
设置器和获取器只是公共字段的微小改进。它们是缺少抽象和准类的明确标志。 - sbi
2
@James:(您需要正确地@回复评论,以便它们显示在我们的“响应”选项卡中。)我能说什么,不是在那篇文章中更优美地表达了吗?另一种选择是提高抽象级别,这样就不再需要访问成员了。类是一些内部(又称“私有”)状态加上公共方法来操作该状态。如果类的用户需要直接访问类的状态,则实际上这是伪装成结构化编程。可悲的是,Java/C#群体因为纯面向对象而感到如此优越,似乎完全忽略了这一点。 - sbi
1
@James:这里所说的“大多数人”指的是在你发布这条信息的java标签中的大多数人。 (顺便说一句,我的发现也很偶然。)正如我所说,这是Java团队发明的一些东西,并且遗憾的是已经扩散到了C#中。一些人认为通过添加公共属性而不是公共数据成员实现了纯面向对象编程。如果你是在c++标签下发帖,“大多数人”应该会同意我的观点。(至少我希望如此。) - sbi
2
@James:如果你有抓取对象内部读写状态的算法,那么基本上就是_结构化编程_。_SP_将代码组织成__数据结构__和在其上操作的__算法__。算法直接操作存储在数据结构中的值。另一方面,_OOP_将_封装_作为其基石之一。这意味着你甚至不应该知道对象状态的内部表示,更不用说去摆弄它了。相反,你调用对象上的方法,以定义良好的方式改变其状态。 - sbi
1
我刚刚看到coobird给出了一个非常好的答案,在这些评论中基本上与我说的一样。 - sbi
显示剩余12条评论
12个回答

21

通常使用getter和setter而不是直接允许其他对象更改您的字段。 当您看到99.99%的getter和setter除了可以通过直接访问字段完成的操作外,可能没有任何意义。 但是当您决定当玩家受到伤害超过一定程度时,他会掉落一半的库存,或者您想限制魔法物品可使用的背包插槽数量时,你必须要么寻找代码中修改字段的所有位置,要么,如果使用getter和setter,您可以在类中完全进行更改。 这就是面向对象编程的核心-您已将对象的“知识”封装在对象本身内,而不是将其分散在与该对象交互的所有对象之间。


我认为编写/生成的getter和setter有一半是无意义的(甚至经常不被调用)。然而,拥有太多比太少要好。在我看来。 - Peter Lawrey
1
@Peter,我承认我经常在Eclipse中使用“生成getter和setter”的选项,我完全同意你的观点。 - Paul Tomblin
我也是,我尽量避免在Eclipse中使用自动生成的方法,但是在我使用时,我会尝试删除所有不需要的get和set方法,并修改我需要的方法使它们更加智能化。(请参见我的上面的编辑) - James
1
@Peter:我认为这是错误的。请看一下我对问题的评论。 - sbi
1
“但是当你决定当玩家受到伤害超过一定程度时,他会掉落一半的库存时会发生什么?”然后设置health成员的方法(不应该是setHealth()方法)会触发对此的检查。 - sbi
2
如果您不能信任访问代码执行正确操作,则getter/setter(或在此情况下为adjuster)是必不可少的。例如,如果代码由另一个开发人员或团队使用,则这是一个好主意。如果您在这两种情况下都是开发人员,但不相信自己能够做到正确或修复它,请使用getter/setter。 ;) 就个人而言,如果字段是不可变的,我更喜欢放弃getter。在另一个模块中访问的可变字段更有可能需要getter/setter。如果您有良好的单元测试,您应该能够通过直接更改字段来检测到可能发生的错误类型。 - Peter Lawrey

18

面向对象编程的核心概念之一是封装——即将对象的状态(例如,对象中的数据)隐藏在外部,并让对象自己处理其状态。

当封装做得好时,对象的状态只能通过对象提供的接口(例如,对象拥有的方法)从外部世界受到影响。

我认为你的代码已经开始使用封装了。

让我们来看看代码

让我们来看看beDamaged方法。

public void beDamaged(double damage)
{
    this.health -= damage;

    if (this.health < 0)
    {
        this.health = 0;
    }
}

这里我们可以看到,这个方法将被外界调用,玩家的生命值会受到影响。它还包含逻辑,所以生命值不能为负数。你编写的玩家beDamaged方法使对象状态保持在你定义的有效状态参数范围内。
让我们推断一些关于玩家的事情
现在,从上面的内容,我认为我可以推断出以下关于玩家对象的信息:
一个玩家的健康值不能为负数。
我们所推断的是否总是正确的?
让我们看看从你提供的代码中是否总是如此。
啊哈!我们有一个小问题:
public double health;

health字段设置为public后,外部世界可以直接操作该字段,以使玩家对象的状态变为可能不希望的状态,例如以下代码:

Player player = new Player();
player.health = -100

我猜测玩家不应处于生命值为负数的状态。

我们能做些什么呢?

如何避免这种情况?-- 通过将health字段设置为private

现在,唯一影响玩家health的方法是通过beDamagedgainHealth方法进行,这可能是外部世界影响玩家健康的正确方式。

这也意味着当你将一个字段设置为private时,并不意味着你必须为该字段创建getter和setter。

私有字段不需要getter和setter

Getter和setter通常是一种直接影响对象拥有的字段的方法,也许带有一些验证来防止错误输入使您的对象处于不应该的状态,但有时对象本身应该负责影响数据,而不是外部实体。


谢谢你的回答和解释,我从未像这样考虑过,但是你不能只使用set方法将其设置为负数吗?或者我应该验证set方法以防止这种情况发生?(是的,你说得对,玩家的健康值不能低于100) - James
2
"我应该验证set方法吗?" --> Bingo!这是使用setter而不是公共字段可以获得的优势;这是验证您的输入并拒绝不良数据以使对象保持在可接受状态的机会。 - coobird
非常感谢,我想我没有说明,但是我已经在VB6中编程了几年,但是现在才真正开始使用Java,所以感谢您的逻辑解释 :) - James
只是想说这是一个非常全面和好的答案。干得好 :) - Accatyyc
这基本上就是我在评论中一直在说的。除了最后一段(如果你有“beDamaged”和“gainHealth”,为什么还需要“health”的setter?),我完全同意。我给你点赞。 - sbi

8
在Java中,使用带有getter/setter的私有字段是推荐的做法,前提是外部客户端确实需要访问这些字段。否则,请将它们保留为私有字段,并且不要提供getter/setter。
这是最佳实践的原因:
1. 如果客户端直接使用您的字段,以后需要更改某些内容,那么您就会陷入困境。使用getter可以在访问字段之前执行很多操作。
2. 有一个叫做JavaBeans规范的东西需要您使用getter/setters。如果没有它们,您的类(然后称为bean)将无法与之交互。JSP和JSF的EL就是需要您的类符合JavaBeans标准的示例。
(p.s. 与您的问题无关,但最好不要将backPack声明为ArrayList。声明为List;代码要接口而不是实现)

1
你的代码只需要知道它正在处理一个列表。无论是数组列表、树形列表、链表还是其他类型的列表都无关紧要。如果你的代码不依赖于特定类型的列表,那么以后很容易将其替换为另一种列表实现方式。这使得你的代码更易于维护,并且在总体上更加健壮。 - Arjan Tijms
好的,谢谢你的回复。那么如果我只把它变成一个列表,是不是就不需要重构我的代码以便正确访问列表了?(编辑:我刚刚检查了一下,似乎没问题了,谢谢你) - James
实际上,在99.9999999%的情况下,您会发现在更改后不需要重构任何内容。这就是证明您根本不需要ArrayList,而只需要List的方法;) - Arjan Tijms
1
你错了,java.util.List绝对有一个isEmpty方法。请参见http://download.oracle.com/javase/6/docs/api/java/util/List.html#isEmpty()。 - Arjan Tijms
哦,我正在使用java.awt.list,我应该使用它吗? - James
显示剩余4条评论

4
如果您有一个带有方法get()set()的私有字段,除了检索和分配值之外不执行任何其他操作,则应将该字段设置为公共,因为该字段实际上并不是私有的,而且getter和setter只会影响性能。如果getter和setter检查正在设置的值或是否允许检索该值,则可以使用getter和setter。例如,如果您有一个变量private int width;,并且有人尝试使用setter输入-1,并且setter确保它不是负数,则这是一个很好的用法。例如:
private int width;
public int get(){
    return width;
}
public void set(int w){
    if (w < 0) throw new RuntimeException();
    else width = w;
}

如果只是简单的赋值或获取值,那么使用getter和setter会对性能造成影响。因此,只有在除了获取或赋值之外还需要进行其他操作时才使用getter和setter。

简而言之:

当需要执行除了获取或赋值之外的其他操作时,请使用getter和setter。否则,请使用公共字段。

例如:

不好的例子:

private int width;
public int get(){
    return width;
}
public void set(int w){
    width = w;
}

GOOD:

private int width;
public int get(){
    return width;
}
public void set(int w){
    if (w < 0) throw new RuntimeException();
    else width = w;
}

如果你只需要获取或设置,那么GOOD就很好用:

public int width;

嗯,这有点与其他人说的相矛盾,但很有道理,谢谢你的回答。 - James
这种做法只适用于在资源非常有限的平台上执行的程序,比如小型设备。否则,方法调用的成本非常小,甚至在99%的情况下都不值得考虑。因此,如果James正在为类似于Java启用的手机创建游戏,则此答案非常有用。 - Goran Jovic
1
此外,我支持反对广泛接受的惯例,即getter和setter不应该执行除设置和检索字段值之外的任何操作。 - Goran Jovic
好的,感谢其他人的解释,你们在解释方面非常友善和有帮助。 - James
@Leo 我同意 getter 和 setter 应该有时用于除了读写字段值之外的其他用途。但我强烈反对公共字段被用于其他用途的说法。 - Sean Patrick Floyd

3
关于这个问题:
事实上,从我所看到的(并且这似乎是合理的),使用私有字段但使用getter和setter来访问它们也不好,因为这违背了使用私有字段的初衷。
主要问题在于,许多开发人员自动生成所有私有字段的getter和setter。如果你打算这样做,我同意,你可能会将字段保持为public(不,public字段甚至更糟)。
对于每个字段,你应该检查:
a) 是否需要Getter(其他类需要知道此字段的值) b) 是否需要Setter(其他类需要能够更改此字段的值) c) 或者字段是否需要是不可变的(final),如果是,则必须在定义或构造函数中初始化(并且显然不能有setter)
但你几乎永远不应该(例外:值对象)假设所有私有字段都有getter和setter,并让你的IDE生成它们。

那似乎是处理问题的逻辑方式,我会确保在使用getter和setter之前先进行思考。 - James

2
使用getter和setter的一个优点是,它更容易调试对字段的写访问。

1

你的方法的缩短版本...

public void beDamaged(double damage) {
    health = Math.max(0, health-damage);
}

public void gainHealth(double gainedHp) {
    health = Math.min(maxHealth, health + gainedHp);
}

甚至包括以下内容,可以使用+1来增加,-1来减少1个hp。

public void adjustHealth(double adjustHp) {
    health = Math.max(0, Math.min(maxHealth, health + adjustHp));
}

1
嗯,谢谢您的评论,虽然我确实喜欢可读性 :S - James
1
@詹姆斯,一旦你习惯使用这些函数,你可能会发现这更清晰。然而,我建议你坚持你认为最清晰的方法。 - Peter Lawrey
好的,谢谢你的建议。我可能需要逐渐习惯这样的函数,然后回头看看以前的项目,会感叹“哇,我以前写的代码简直就是一团糟” :P 但现在我还是先保持原样吧。不过,你对于 getters 和 setters 有什么看法呢? - James
2
@James,当你的对象应该/可能包含一些逻辑时,getter/setter 是有用且必需的。它们对于数据值对象来说不太有用。有些人过度使用它们或坚持认为你必须在任何地方都要使用它们,因为有一天你可能会用到它们。然而,我相信未使用和未经测试的代码比没有代码更糟糕。 - Peter Lawrey
好的,谢谢您对此事的看法,这似乎是合乎逻辑的推理。 - James

1

私有字段和设置器以及获取器确实是您最好的选择。

进一步注意,这通常是任何语言中的良好代码,因为它可以保持您的安全性并为您提供更易于调试和维护的结构。(别忘了记录文档!)

总之,使用设置器和获取器,即使您找到其他选项,也是一个好习惯。


你所说的“文档”,是指生成JavaDoc吗? - James
1
仅仅是对你的代码进行注释。你会自己学会这个技能,但我仍然想强调尽可能多地进行注释的重要性。如果你有编程朋友,请注释所有的代码,这样他们就可以直接获取代码并理解它,而不需要与你交谈。几年后,当你回到自己的代码时,即使完全不记得它,你也会感谢自己。 - Mantar
啊,不用担心,我知道注释的重要性,我在VB6方面相当熟练,但是我只是最近才开始接触Java。 - James
太好了。:) 昨天我的代码打了自己一拳,因为去年我太懒惰了,所以我试图告诉每个人这种危险。:P (现在回到找出 a[f].q 去哪里的问题上) - Mantar

1

Getter和Setter是您的类的公共接口的一部分。这是类设计者/开发者与该类的用户之间的一个协议。定义Getter和Setter时,您应致力于在未来版本中维护它们。

属性只应对给定版本的类的实现进行相应。通过这种方式,类开发人员可以单方面更改实现,因此字段,而不会违背维护接口的承诺。

这里是一个例子。考虑一个名为Point的类。如果您决定Point具有x和y公共属性,那么您可能永远不会更改此属性。相反,如果您拥有get/set X/Y方法,则类的后续版本可以使用各种内部表示:矩形坐标(x,y),但也包括极坐标(r,theta)等。所有这些都不修改公共接口。


1

如果您没有维护任何不变量,那么公共字段是最好的选择。如果您需要跨多个成员保持不变,则需要私有字段和封装。

但是,如果您无法想出比 GetFoo 和 SetFoo 更好的名称来命名方法,那么这很可能意味着您的 getter 和 setter 是毫无价值的。


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