Как избежать «локальная переменная, возможно, не была инициализирована»?

/*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 предлагает присвоить значения 0 для textCost и linkCost. Есть ли другой способ обойти эту проблему. Если я не назначу 0 значений, они получат ошибку (возможно, локальная переменная XXX не была инициализирована). Может ли кто-нибудь объяснить мне, почему это происходит, хотя обеим переменным присвоены уравнения?

Спасибо.

РЕДАКТИРОВАТЬ: я сделал, как было предложено, и объявил переменные только тогда, когда мне это понадобится. Я также добавил некоторые комментарии.


person HelloWorld    schedule 18.10.2009    source источник
comment
Я обновил свой ответ с большим количеством рефакторинга. Надеюсь, вам понравится.   -  person Jon Skeet    schedule 19.10.2009


Ответы (8)


Три предложения, прежде чем я углублюсь в код:

  • Объявляйте переменные как можно позже, чтобы облегчить понимание кода.
  • Рефакторинг этого гигантского метода - на данный момент он нечитаемо огромный.
  • Сделайте константы 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 с плавающей запятой для получения дополнительной информации - все это действительно имеет отношение к Java. .
  • Само объявление теперь инкапсулировано в классе. Вы можете добавить гораздо больше информации здесь по мере необходимости.
  • Код находится в пакете. По общему признанию, на данный момент все это находится в одном файле (поэтому только класс EntryPoint является общедоступным), но это только ради переполнения стека и мне не нужно открывать Eclipse.
  • Есть JavaDoc, объясняющий, что происходит, по крайней мере, для нескольких методов. (Вероятно, я бы добавил еще немного в реальном коде. У меня мало времени.)
  • Мы проверяем пользовательский ввод, за исключением количества слов, и мы выполняем эту проверку в одной процедуре, которая должна быть разумно тестируемой. Затем мы можем предположить, что всякий раз, когда мы запрашивали ввод, мы получали что-то действительное.
  • Количество статических методов в EntryPoint немного настораживает. Это не кажется ужасно ООП, но я нахожу, что это часто происходит в точке входа в программу. Обратите внимание, что здесь нет ничего общего с комиссией — в основном это просто пользовательский интерфейс.

Здесь больше кода, чем было раньше, но это (ИМО) гораздо более читаемый и поддерживаемый код.

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);
        }
    }
}
person Jon Skeet    schedule 18.10.2009
comment
Я не думаю, что оба предложения - хорошие идеи. Предупреждение о том, что переменная не была объявлена, важно для предотвращения сбоев (неинициализированных, считанных). Цикломатическое число метода, по моим подсчетам, равно 7, так что функция тоже не такая уж большая. - person Red33mer; 18.10.2009
comment
Цикломатическая сложность — это не предел удобочитаемости. Метод состоит из 125 строк, и я уверен, что его можно очень разбить на части. Объявление переменных как можно позже не удаляет предупреждение о неинициализированных переменных — оно просто упрощает понимание этого предупреждения. Я поддерживаю оба моих предложения. - person Jon Skeet; 18.10.2009
comment
... и теперь я добавляю третью. - person Jon Skeet; 18.10.2009
comment
Еще одна причина для разделения — удобство тестирования. Нынешний метод в принципе не поддается проверке. (Да, вы могли сделать это, но это было бы безумием.) Разделение метода на логические фрагменты для обработки каждого значения отдельно от его входных данных сделает его много. >проще проверить. - person Jon Skeet; 18.10.2009
comment
Спасибо, Джон. Прошел всего месяц, как я начала учиться. Так что некоторые предложения все еще кажутся мне немного чуждыми, но хорошо, что я узнаю что-то новое. Я до сих пор не знаю, как создать несколько методов и как правильно их разделить. - person HelloWorld; 18.10.2009
comment
@HelloWorld - не беспокойся об этом, ты доберешься. Но я бы серьезно посоветовал вам больше узнать о создании других методов и типов, прежде чем идти дальше. В частности, не пытайтесь приступить к реальному программированию полезных вещей, пока не изучите основы. - person Jon Skeet; 18.10.2009
comment
Будет ли полезно, если я попытаюсь взять ваш образец кода и реорганизовать его, чтобы показать вам, как он может выглядеть? - person Jon Skeet; 18.10.2009
comment
+1 Будучи новичком в основной Java, я очень благодарен за четкие и краткие ответы. В закладки :-) - person Edward J Beckett; 26.10.2012

linkCost не инициализируется, если link == 'y' и advPay не являются 'y' или 'n'.

Другими словами, вы получаете эту ошибку, когда компилятор может найти путь через ваш код, где локальная переменная не инициализируется до ее использования.

person eljenso    schedule 18.10.2009

это происходит потому, что присваивание происходит внутри условного оператора, если условие не выполняется, присваивание никогда не происходит.

чтобы избежать ошибки, вы должны присвоить значение (наиболее распространенным будет 0) вне условного выражения.

person josemrb    schedule 18.10.2009

Анализ, который Eclipse выполняет, чтобы определить, назначена ли переменная на каждом пути кода, недостаточно интеллектуален, чтобы понять, что тесты на numberOfWords никогда не завершатся неудачно. Как правило, поскольку невозможно статически оценить все возможные условия, компилятор/программа проверки синтаксиса не будет пытаться оценить ни одно из них. Если вы заменили последнее «иначе, если» на «иначе», это должно сработать, так как по крайней мере одно присвоение textCost произойдет независимо от проверяемых условий.

person Nicholas Riley    schedule 18.10.2009
comment
Я сделал, как вы предложили для textCost, и заменил else на else. Спасибо. - person HelloWorld; 18.10.2009

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 <= 35) и (numberOfWords > 35) охватывают все возможности.

Вы можете либо инициализировать его значением 0 при объявлении, либо включить дополнительный else {}, который устанавливает его равным нулю.

Аналогичное объяснение для другой переменной.

person hexium    schedule 18.10.2009
comment
Думаю, вы хотели сказать (numberOfWords ‹= 35) и (numberOfWords › 35). Блин копипаст ;) - person ThisSuitIsBlackNot; 18.10.2009
comment
Спасибо, я тоже это заметил и исправил, как вы ответили. :) - person hexium; 18.10.2009
comment
Кроме того, вы можете заменить else if (numberOfWords > 35) только на else. И Eclipse, вероятно, не может признать, что все возможности покрыты; На самом деле в Java есть определенные правила о том, когда выдавать a, возможно, не было инициализировано, и этот случай может привести к их возникновению. - person JaakkoK; 18.10.2009

сообщение об ошибке говорит вам, что эти переменные не всегда инициализируются. Это потому, что ваша инициализация происходит только при определенных условиях (они находятся в операторах if). Надеюсь это поможет..

person andyp    schedule 18.10.2009

Даже если вы знаете, что одна из 3 ветвей сравнения с numberOfWords будет посещена, компилятор этого не знает. Он ошибочно предположит, что можно ввести предложение else, и переменная textCost останется единичной.

Аналогично с switch (advPay). Даже если вы знаете, что один из двух будет посещен, компилятор этого не знает.

Предложение: удалите else if (numberOfWords > 35), сделайте его просто else.

Что касается switch (advPay), добавьте случай default. Внутри можно поставить throw new AssertionError();.

person RichN    schedule 18.10.2009
comment
Было бы лучше, если бы я просто удалил оператор switch и просто создал вложенный оператор if? - person HelloWorld; 18.10.2009
comment
На самом деле не имеет никакого значения. Тем не менее, вам все еще нужно предложение else. - person RichN; 18.10.2009

Хороший способ избежать таких проблем – установить для присваиваемой переменной значение 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 - это другое)... но компилятор - ваш союзник, а не враг...

person Christophe Roussy    schedule 11.08.2014