【问题标题】:How to avoid 'the local variable may not have been initialized'?如何避免“局部变量可能尚未初始化”?
【发布时间】:2009-10-18 17:34:29
【问题描述】:
/*This is a program that calculates Internet advertising rates based on what features/options you choose.
 * 
 *  
 */

import java.util.Scanner;

public class InternetAdvertising 
{
    public static void main(String[] args)
    {
        Scanner in = new Scanner(System.in);

        int numberOfWords;      

        //I assigned 0 values to both as Eclipse suggested
        float textCost = 0;
        float linkCost = 0;     

        float graphicCost;

        //<=25 words is a flat fee of $.40 per word plus Base fee of $3.00 
        final float TEXT_FLAT_FEE = 0.40F;
        final float TEXT_BASE_FEE = 3.00F;

        //<=35 words is $.40 for the first 25 words and 
        //an additional $.35 per word up to and including 35 words plus Base fee of $3.00 
        final float LESS_OR_EQUAL_THAN_THIRTYFIVE = 0.35F;

        //Over 35 words is a flat fee of $.32 per word with no base fee
        final float MORE_THAN_THIRTYFIVE = 0.32F;


        System.out.println("Welcome!");

        System.out.print("Enter the number of words in your ad: ");
        numberOfWords = in.nextInt();

        if (numberOfWords <= 25)
        {
            textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * numberOfWords);
        }

        else if (numberOfWords <= 35)
        {
            textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * 25) + (numberOfWords - 25) * LESS_OR_EQUAL_THAN_THIRTYFIVE;
        }

        else if (numberOfWords > 35)
        {
            textCost = numberOfWords * MORE_THAN_THIRTYFIVE;
        }


        String addLink, advancePay;
        char link, advPay;

        final float LINK_FLAT_FEE = 14.95F;
        final float THREE_MONTH_ADV_DISCOUNT = 0.10F;

        System.out.print("Would you like to add a link (y = yes or n = no)? ");
        addLink = in.next();

        link = addLink.charAt(0);
        link = Character.toLowerCase(link); 

        if (link == 'y')
        {
            System.out.print("Would you like to pay 3 months in advance " + "(y = yes or n = no)? ");
            advancePay = in.next();

            advPay = advancePay.charAt(0);
            advPay = Character.toLowerCase(advPay);

            switch (advPay)
            {
                case 'y':

                    linkCost = (3 * LINK_FLAT_FEE) - (3 * LINK_FLAT_FEE) * THREE_MONTH_ADV_DISCOUNT;

                    break;

                case 'n':

                    linkCost = LINK_FLAT_FEE;

                    break;
            }               
        }

        else
        {
            linkCost = 0;
        }


        String addGraphic;
        char graphic;

        System.out.print("Would you like to add graphics/pictures” + “(S = Small, M = Medium, L = Large or N = None)? ");
        addGraphic = in.next();

        graphic = addGraphic.charAt(0);
        graphic = Character.toUpperCase(graphic);
        graphic = Character.toLowerCase(graphic);       
        switch (graphic)
        {
            case 's':

                graphicCost = 19.07F;

                break;

            case 'm':

                graphicCost = 24.76F;

                break;

            case 'l':

                graphicCost = 29.33F;

                break;

            default:
                graphicCost = 0;
        }


        float gst, totalBeforeGst, totalAfterGst;

        final float GST_RATE = 0.05F;

        totalBeforeGst = textCost + linkCost + graphicCost; //textCost & linkCost would not initialize

        gst = totalBeforeGst * GST_RATE;

        totalAfterGst = totalBeforeGst + (totalBeforeGst * GST_RATE);


        System.out.printf("\t\t%-16s %11s\n", "Category", "Cost");
        System.out.printf("\t\t%-16s %11.2f\n", "Text", textCost);  //linkCost would not initialize
        System.out.printf("\t\t%-16s %11.2f\n", "Link", linkCost);  //textCost would not initialize 
        System.out.printf("\t\t%-16s %11.2f\n", "Graphic", graphicCost);
        System.out.printf("\t\t%-16s %11.2f\n", "Total", totalBeforeGst);
        System.out.printf("\t\t%-16s %11.2f\n", "GST", gst);
        System.out.printf("\t\t%-16s %11.2f\n", "Total with GST", totalAfterGst);
    }   
}

我几乎完成了这段代码,Eclipse 建议我为 textCostlinkCost 分配 0 值。有没有其他方法可以解决这个问题。如果我不分配 0 值,他们会收到错误(局部变量 XXX 可能尚未初始化)。有人可以向我解释为什么会发生这种情况,即使我为两个变量都分配了方程式?

谢谢。

编辑:我按照建议做了,只在需要时才声明变量。我还添加了一些 cmets。

【问题讨论】:

  • 我已经通过大量重构更新了我的答案。希望你喜欢。

标签: java eclipse


【解决方案1】:

在我深入研究代码之前的三个建议:

  • 尽可能晚地声明变量,以便更容易理解代码。
  • 重构这个巨大的方法 - 目前它的规模大得无法阅读。
  • 制作常量static final 字段。它们与对该方法的任何特定调用无关,因此它们不应是局部变量。

现在关于实际问题,最简单的方法是确保每个可能的流程实际上确实分配了一个值或抛出了一个异常。所以对于textCost,将代码更改为:

if (numberOfWords <= 25)
{
    textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * numberOfWords);
}
else if (numberOfWords <= 35)
{
    textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * 25) + (numberOfWords - 25) * 
               LESS_OR_EQUAL_THAN_THIRTYFIVE;
}
else // Note - no condition.
{
    textCost = numberOfWords * MORE_THAN_THIRTYFIVE;
}

对于 linkCost,将 switch 语句更改为:

switch (advPay)
{
    case 'y':
        linkCost = (3 * LINK_FLAT_FEE) - 
                   (3 * LINK_FLAT_FEE) * THREE_MONTH_ADV_DISCOUNT;
        break;
    case 'n':
        linkCost = LINK_FLAT_FEE;
        break;
    default:
        throw new Exception("Invalid value specified: " + advPay);
}

现在您可能不想在这里抛出异常。您可能想再次循环,或类似的东西。您可能只想使用纯Exception - 但您应该考虑确实想要使用的确切异常类型。

总是不可能做到这一点。编译器确定明确分配的规则相对很简单。如果您确实无法更改代码以使编译器满意,则可以分配一个虚拟初始值。我建议尽可能避免这种情况。在您的第一种情况下,实际上总是会分配该值 - 但在第二种情况下,您确实没有advPay既不是'y'也不是'n'时给出一个值,这可能导致以后很难诊断的问题。编译器错误可帮助您发现此类问题。

不过,我强烈建议您重构此方法。我怀疑当每个方法中只有大约 10 行代码需要推理,并且每个变量在其第一次使用之前或第一次使用时声明时,你会发现为什么没有明确分配事物会更容易理解。

编辑:

好的,彻底重构的代码如下。我不会声称它是世界上最好的代码,但是:

  • 它更易于测试。您可以轻松地为它的每个部分编写单元测试。 printAllCosts 不是很容易测试,但您可能会遇到过载,需要将 Writer 打印到 - 这会有所帮助。
  • 每个计算位都在一个逻辑位置。链接和图形有一小部分可能的值——Java 枚举在这里很合适。 (我知道它们很可能超出了您当前的技能水平,但很高兴看到可用的内容。)
  • 我不再使用二进制浮点数,因为它们不适用于数字。相反,我在任何地方都使用整数美分并转换为BigDecimal 用于显示目的。有关更多信息,请参阅我在 .NET floating point 上的文章 - 这一切都与 Java 相关。
  • 广告本身现在封装在一个类中。您可以在需要时在此处添加更多信息。
  • 代码在一个包中。诚然,目前它们都在一个文件中(这就是为什么只有 EntryPoint 类是公开的)但这只是为了 Stack Overflow 而我不必打开 Eclipse。
  • JavaDoc 解释了正在发生的事情 - 至少对于一些方法。 (我可能会在实际代码中添加更多内容。我的时间不多了。)
  • 我们验证用户输入,除了字数 - 我们在单个例程中执行该验证,这应该是可合理测试的。然后我们可以假设,每当我们要求输入时,我们就得到了有效的信息。
  • EntryPoint 中的静态方法数量略显惊人。 OO 感觉不是很糟糕——但我发现这通常是围绕程序入口点的方式。请注意,那里与费用无关 - 它只是基本上是用户界面。

这里的代码比以前更多 - 但它 (IMO) 的代码可读性和可维护性要高得多。

package advertising;

import java.util.Scanner;
import java.math.BigDecimal;

/** The graphic style of an advert. */
enum Graphic
{
    NONE(0),
    SMALL(1907),
    MEDIUM(2476),
    LARGE(2933);

    private final int cost;

    private Graphic(int cost)
    {
        this.cost = cost;
    }

    /** Returns the cost in cents. */
    public int getCost()
    {
        return cost;
    }
}

/** The link payment plan for an advert. */
enum LinkPlan
{
    NONE(0),
    PREPAID(1495), // 1 month
    POSTPAID(1495 * 3 - (1495 * 3) / 10); // 10% discount for 3 months up-front

    private final int cost;

    private LinkPlan(int cost)
    {
        this.cost = cost;
    }

    /** Returns the cost in cents. */
    public int getCost()
    {
        return cost;
    }
}

class Advertisement
{
    private final int wordCount;
    private final LinkPlan linkPlan;
    private final Graphic graphic;

    public Advertisement(int wordCount, LinkPlan linkPlan, Graphic graphic)
    {
        this.wordCount = wordCount;
        this.linkPlan = linkPlan;
        this.graphic = graphic;
    }

    /**
     * Returns the fee for the words in the advert, in cents.
     * 
     * For up to 25 words, there's a flat fee of 40c per word and a base fee
     * of $3.00.
     * 
     * For 26-35 words inclusive, the fee for the first 25 words is as before,
     * but the per-word fee goes down to 35c for words 26-35.
     * 
     * For more than 35 words, there's a flat fee of 32c per word, and no
     * base fee.     
     */
    public int getWordCost()
    {
        if (wordCount > 35)
        {
            return 32 * wordCount;
        }
        // Apply flat fee always, then up to 25 words at 40 cents,
        // then the rest at 35 cents.
        return 300 + Math.min(wordCount, 25) * 40
                   + Math.min(wordCount - 25, 0) * 35;        
    }

    /**
     * Displays the costs associated with this advert.
     */
    public void printAllCosts()
    {
        System.out.printf("\t\t%-16s %11s\n", "Category", "Cost");
        printCost("Text", getWordCost());
        printCost("Link", linkPlan.getCost());
        printCost("Graphic", graphic.getCost());
        int total = getWordCost() + linkPlan.getCost() + graphic.getCost();
        printCost("Total", total);
        int gst = total / 20;
        printCost("GST", gst);
        printCost("Total with GST", total + gst);
    }

    private void printCost(String category, int cents)
    {
        BigDecimal dollars = new BigDecimal(cents).scaleByPowerOfTen(-2);
        System.out.printf("\t\t%-16s %11.2f\n", category, dollars);
    }
}

/**
 * The entry point for the program - takes user input, builds an 
 * Advertisement, and displays its cost.
 */
public class EntryPoint
{
    public static void main(String[] args)
    {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Welcome!");
        int wordCount = readWordCount(scanner);
        LinkPlan linkPlan = readLinkPlan(scanner);
        Graphic graphic = readGraphic(scanner);

        Advertisement advert = new Advertisement(wordCount, linkPlan, graphic);
        advert.printAllCosts();
    }

    private static int readWordCount(Scanner scanner)
    {
        System.out.print("Enter the number of words in your ad: ");
        // Could add validation code in here
        return scanner.nextInt();
    }

    private static LinkPlan readLinkPlan(Scanner scanner)
    {
        System.out.print("Would you like to add a link (y = yes or n = no)? ");
        char addLink = readSingleCharacter(scanner, "yn");
        LinkPlan linkPlan;
        if (addLink == 'n')
        {
            return LinkPlan.NONE;
        }
        System.out.print("Would you like to pay 3 months in advance " +
                         "(y = yes or n = no)? ");
        char advancePay = readSingleCharacter(scanner, "yn");
        return advancePay == 'y' ? LinkPlan.PREPAID : LinkPlan.POSTPAID;
    }

    private static Graphic readGraphic(Scanner scanner)
    {
        System.out.print("Would you like to add graphics/pictures? " +
            "(s = small, m = medium, l = large or n = None)? ");
        char graphic = readSingleCharacter(scanner, "smln");
        switch (graphic)
        {
            case 's': return Graphic.SMALL;
            case 'm': return Graphic.MEDIUM;
            case 'l': return Graphic.LARGE;
            case 'n': return Graphic.NONE;
            default:
                throw new IllegalStateException("Unexpected state; graphic=" +
                                                graphic);
        }
    }

    private static char readSingleCharacter(Scanner scanner,
                                            String validOptions)
    {
        while(true)
        {
            String input = scanner.next();
            if (input.length() != 1 || !validOptions.contains(input))
            {
                System.out.print("Invalid value. Please try again: ");
                continue;
            }
            return input.charAt(0);
        }
    }
}

【讨论】:

  • 我不认为,这两个建议都是好主意。未声明变量的警告对于防止您(未初始化,读取)失败非常重要。该方法的圈数,据我统计,是 7,所以函数也不是很大。
  • 圈复杂度并不是可读性的全部。该方法有 125 行长,我确信它可以很容易地分解非常。尽可能晚地声明变量并不会消除有关未初始化变量的警告——它只会使该警告易于理解。我支持我的两个建议。
  • ...现在我要添加第三个。
  • 拆分它的另一个原因是为了可测试性。目前的方法基本上是无法测试的。 (是的,你可以做到这一点,但这太疯狂了。)将方法拆分为逻辑块以分别从输入数据中计算出每个值将使它很多 更容易测试。
  • 如果我尝试获取您的示例代码并对其进行重构以向您展示它的外观会有所帮助吗?
【解决方案2】:

link == 'y'advPay 不是 'y''n' 时,linkCost 未初始化。

换句话说,当编译器可以通过您的代码找到一个路径,其中局部变量在使用之前未初始化时,您会收到此错误。

【讨论】:

    【解决方案3】:

    这是因为赋值发生在条件内,如果条件不满足,则永远不会发生赋值

    为避免错误,您必须在条件之外指定一个值(最常见的是 0)。

    【讨论】:

      【解决方案4】:

      Eclipse 为确定是否在每个代码路径上分配变量而执行的分析不够智能,无法意识到numberOfWords 上的测试永远不会全部失败。通常,因为不可能静态评估每个可能的条件,编译器/语法检查器不会尝试评估其中的任何一个。如果您将最后一个“else if”替换为“else”,它应该可以工作,因为无论正在测试的条件如何,都会发生至少一个对 textCost 的分配。

      【讨论】:

      • 我按照你对 textCost 的建议做了,如果用 else 替换了 else。谢谢。
      【解决方案5】:

      Eclipse 会警告您,因为您的初始化是在条件句中进行的。如果不满足任何条件,textCost 将被取消初始化。

      if (numberOfWords <= 25)
          {
                  //assign a value to textCost
          }
          else if (numberOfWords <= 35)
          {
                  //assign a value to textCost
          }
          else if (numberOfWords > 35)
          {
                  //assign a value to textCost
          }
      

      Eclipse 可能没有认识到(numberOfWords &lt;= 35)(numberOfWords &gt; 35) 涵盖了所有可能性。

      您可以在声明时将其初始化为 0,或者包含一个额外的 else {} 将其设置为零。

      其他变量的类似解释。

      【讨论】:

      • 您的意思是说 (numberOfWords 35)。该死的复制/粘贴;)
      • 谢谢,我也注意到了这一点,并且正在按照您的回复进行修复。 :)
      • 另外,您可以将 else if (numberOfWords &gt; 35) 替换为 else。并且 Eclipse 可能不允许承认所有的可能性都被覆盖了。 Java 实际上已经指定了何时发出“可能尚未初始化”的规则,这种情况可能会遇到这些问题。
      【解决方案6】:

      错误消息告诉您,这些变量并非始终初始化。这是因为您的初始化仅在某些条件下发生(它们位于 if 语句中)。希望这会有所帮助..

      【讨论】:

        【解决方案7】:

        即使您知道将访问与 numberOfWords 进行比较的 3 个分支之一,但编译器并不知道这一点。它会错误地假设可以输入 else 子句,并且 textCost 变量将保持统一。

        switch (advPay) 类似。 即使您知道将访问两者之一,但编译器不会。

        建议: 删除 else if (numberOfWords &gt; 35) 使其成为 else

        至于switch (advPay),添加default case。你可以在里面放一个throw new AssertionError();

        【讨论】:

        • 如果我只删除switch语句并创建一个嵌套的if语句会更好吗?
        • 确实没有任何区别。不过,你仍然需要一个 else 子句。
        【解决方案8】:

        避免此类问题的一个好方法是在检查之前将要分配的变量设置为final未初始化。这将强制您在使用/读取之前设置一个值。

        final textCostTmp;
        if (condition1) {
          textCostTmp = ...;
        } else if (condition2) {
          textCostTmp = ...;
        } 
        // if you use textCostTmp here the compiler will complain that it is uninitialized !
        textCost = textCostTmp;
        

        要解决这个问题,请不要初始化变量,因为您可能会错过缺少的 else 案例。 唯一合适的解决方案是添加一个else 案例以涵盖所有可能的案例! 我认为初始化非最终变量是不好的做法,除了一些罕见的情况,比如循环中的计数器。

        建议的方法将迫使您处理现在和将来的所有可能情况(更易于维护)。 编译器有时有点愚蠢(无法确定 numberOfWords > 35 是 else)...但是编译器是您的盟友而不是您的敌人...

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多